mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-31 07:37:24 +00:00
Python: Add support for enums
Support for enums is three-fold: - Enums are dynamically created by invoking `enum.Enum("NameOfEnum", [("Variant1", "Variant1"), ("Variant2", "Variant2"), ...])` and the exposed in the module (next to structs). - They're converted back to `Value::EnumerationValue(enum_name, enum_value)` by checking if a supplied value is an instance of `enum.Enum` and then querying the name and `__class__.name` fields. - When coverting a `Value::EnumerationValue` to Python, the hard work begins: We now pass along a TypeCollection through all phases of the type conversion, which knows about the enums created earlier and tries to locate the correct type class and then variant. Fixes #5828
This commit is contained in:
parent
14e1ae3bc0
commit
82ae731fcb
10 changed files with 504 additions and 183 deletions
|
@ -295,6 +295,37 @@ data.age = 10
|
|||
main_window.data = data
|
||||
```
|
||||
|
||||
### Enums
|
||||
|
||||
Enums declared in Slint and exposed to Python via `export` are then accessible in the namespace that is returned
|
||||
when [instantiating a component](#instantiating-a-component). The enums are subclasses of [enum.Enum](https://docs.python.org/3/library/enum.html).
|
||||
|
||||
**`app.slint`**
|
||||
|
||||
```slint
|
||||
export enum MyOption {
|
||||
Variant1,
|
||||
Variant2
|
||||
}
|
||||
|
||||
export component MainWindow inherits Window {
|
||||
in-out property <MyOption> data;
|
||||
}
|
||||
```
|
||||
|
||||
**`main.py`**
|
||||
|
||||
Variants of the exported `MyOption` enum can be constructed as follows:
|
||||
|
||||
```python
|
||||
import slint
|
||||
# Look for for `app.slint` in `sys.path`:
|
||||
main_window = slint.loader.app.MainWindow()
|
||||
|
||||
value = slint.loader.app.MyOption.Variant2
|
||||
main_window.data = value
|
||||
```
|
||||
|
||||
## Third-Party Licenses
|
||||
|
||||
For a list of the third-party licenses of all dependencies, see the separate [Third-Party Licenses page](thirdparty.html).
|
||||
|
|
|
@ -21,7 +21,7 @@ use pyo3::PyTraverseError;
|
|||
use crate::errors::{
|
||||
PyGetPropertyError, PyInvokeError, PyPlatformError, PySetCallbackError, PySetPropertyError,
|
||||
};
|
||||
use crate::value::{PyStruct, PyToSlintValue, SlintToPyValue};
|
||||
use crate::value::{SlintToPyValue, TypeCollection};
|
||||
|
||||
#[gen_stub_pyclass]
|
||||
#[pyclass(unsendable)]
|
||||
|
@ -72,14 +72,20 @@ impl Compiler {
|
|||
self.compiler.set_translation_domain(domain)
|
||||
}
|
||||
|
||||
fn build_from_path(&mut self, path: PathBuf) -> CompilationResult {
|
||||
CompilationResult { result: spin_on::spin_on(self.compiler.build_from_path(path)) }
|
||||
fn build_from_path(&mut self, py: Python<'_>, path: PathBuf) -> CompilationResult {
|
||||
CompilationResult::new(spin_on::spin_on(self.compiler.build_from_path(path)), py)
|
||||
}
|
||||
|
||||
fn build_from_source(&mut self, source_code: String, path: PathBuf) -> CompilationResult {
|
||||
CompilationResult {
|
||||
result: spin_on::spin_on(self.compiler.build_from_source(source_code, path)),
|
||||
}
|
||||
fn build_from_source(
|
||||
&mut self,
|
||||
py: Python<'_>,
|
||||
source_code: String,
|
||||
path: PathBuf,
|
||||
) -> CompilationResult {
|
||||
CompilationResult::new(
|
||||
spin_on::spin_on(self.compiler.build_from_source(source_code, path)),
|
||||
py,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,6 +143,14 @@ pub enum PyDiagnosticLevel {
|
|||
#[pyclass(unsendable)]
|
||||
pub struct CompilationResult {
|
||||
result: slint_interpreter::CompilationResult,
|
||||
type_collection: TypeCollection,
|
||||
}
|
||||
|
||||
impl CompilationResult {
|
||||
fn new(result: slint_interpreter::CompilationResult, py: Python<'_>) -> Self {
|
||||
let type_collection = TypeCollection::new(&result, py);
|
||||
Self { result, type_collection }
|
||||
}
|
||||
}
|
||||
|
||||
#[gen_stub_pymethods]
|
||||
|
@ -148,7 +162,10 @@ impl CompilationResult {
|
|||
}
|
||||
|
||||
fn component(&self, name: &str) -> Option<ComponentDefinition> {
|
||||
self.result.component(name).map(|definition| ComponentDefinition { definition })
|
||||
self.result.component(name).map(|definition| ComponentDefinition {
|
||||
definition,
|
||||
type_collection: self.type_collection.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
#[getter]
|
||||
|
@ -157,40 +174,41 @@ impl CompilationResult {
|
|||
}
|
||||
|
||||
#[getter]
|
||||
fn structs_and_enums<'py>(&self, py: Python<'py>) -> HashMap<String, Bound<'py, PyAny>> {
|
||||
let structs_and_enums =
|
||||
self.result.structs_and_enums(i_slint_core::InternalToken {}).collect::<Vec<_>>();
|
||||
fn structs_and_enums<'py>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
) -> (HashMap<String, Bound<'py, PyAny>>, HashMap<String, Bound<'py, PyAny>>) {
|
||||
let mut structs = HashMap::new();
|
||||
|
||||
fn convert_type<'py>(py: Python<'py>, ty: &Type) -> Option<(String, Bound<'py, PyAny>)> {
|
||||
match ty {
|
||||
for struct_or_enum in self.result.structs_and_enums(i_slint_core::InternalToken {}) {
|
||||
match struct_or_enum {
|
||||
Type::Struct(s) if s.name.is_some() && s.node.is_some() => {
|
||||
let struct_instance = PyStruct::from(slint_interpreter::Struct::from_iter(
|
||||
s.fields.iter().map(|(name, field_type)| {
|
||||
(
|
||||
name.to_string(),
|
||||
slint_interpreter::default_value_for_type(field_type),
|
||||
)
|
||||
}),
|
||||
));
|
||||
let struct_instance =
|
||||
self.type_collection.struct_to_py(slint_interpreter::Struct::from_iter(
|
||||
s.fields.iter().map(|(name, field_type)| {
|
||||
(
|
||||
name.to_string(),
|
||||
slint_interpreter::default_value_for_type(field_type),
|
||||
)
|
||||
}),
|
||||
));
|
||||
|
||||
return Some((
|
||||
structs.insert(
|
||||
s.name.as_ref().unwrap().to_string(),
|
||||
struct_instance.into_bound_py_any(py).unwrap(),
|
||||
));
|
||||
}
|
||||
Type::Enumeration(_en) => {
|
||||
// TODO
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
structs_and_enums
|
||||
.iter()
|
||||
.filter_map(|ty| convert_type(py, ty))
|
||||
.into_iter()
|
||||
.collect::<HashMap<String, Bound<'py, PyAny>>>()
|
||||
(
|
||||
structs,
|
||||
self.type_collection
|
||||
.enums()
|
||||
.map(|(name, enum_cls)| (name.clone(), enum_cls.into_bound_py_any(py).unwrap()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
|
@ -203,6 +221,7 @@ impl CompilationResult {
|
|||
#[pyclass(unsendable)]
|
||||
pub struct ComponentDefinition {
|
||||
definition: slint_interpreter::ComponentDefinition,
|
||||
type_collection: TypeCollection,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
|
@ -214,7 +233,10 @@ impl ComponentDefinition {
|
|||
|
||||
#[getter]
|
||||
fn properties(&self) -> IndexMap<String, PyValueType> {
|
||||
self.definition.properties().map(|(name, ty)| (name, ty.into())).collect()
|
||||
self.definition
|
||||
.properties_and_callbacks()
|
||||
.filter_map(|(name, (ty, _))| ty.is_property_type().then(|| (name, ty.into())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
|
@ -233,9 +255,11 @@ impl ComponentDefinition {
|
|||
}
|
||||
|
||||
fn global_properties(&self, name: &str) -> Option<IndexMap<String, PyValueType>> {
|
||||
self.definition
|
||||
.global_properties(name)
|
||||
.map(|propiter| propiter.map(|(name, ty)| (name, ty.into())).collect())
|
||||
self.definition.global_properties_and_callbacks(name).map(|propiter| {
|
||||
propiter
|
||||
.filter_map(|(name, (ty, _))| ty.is_property_type().then(|| (name, ty.into())))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn global_callbacks(&self, name: &str) -> Option<Vec<String>> {
|
||||
|
@ -249,8 +273,12 @@ impl ComponentDefinition {
|
|||
fn create(&self) -> Result<ComponentInstance, crate::errors::PyPlatformError> {
|
||||
Ok(ComponentInstance {
|
||||
instance: self.definition.create()?,
|
||||
callbacks: Default::default(),
|
||||
callbacks: GcVisibleCallbacks {
|
||||
callables: Default::default(),
|
||||
type_collection: self.type_collection.clone(),
|
||||
},
|
||||
global_callbacks: Default::default(),
|
||||
type_collection: self.type_collection.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -267,19 +295,29 @@ pub enum PyValueType {
|
|||
Struct,
|
||||
Brush,
|
||||
Image,
|
||||
Enumeration,
|
||||
}
|
||||
|
||||
impl From<slint_interpreter::ValueType> for PyValueType {
|
||||
fn from(value: slint_interpreter::ValueType) -> Self {
|
||||
match value {
|
||||
slint_interpreter::ValueType::Bool => PyValueType::Bool,
|
||||
slint_interpreter::ValueType::Void => PyValueType::Void,
|
||||
slint_interpreter::ValueType::Number => PyValueType::Number,
|
||||
slint_interpreter::ValueType::String => PyValueType::String,
|
||||
slint_interpreter::ValueType::Model => PyValueType::Model,
|
||||
slint_interpreter::ValueType::Struct => PyValueType::Struct,
|
||||
slint_interpreter::ValueType::Brush => PyValueType::Brush,
|
||||
slint_interpreter::ValueType::Image => PyValueType::Image,
|
||||
impl From<i_slint_compiler::langtype::Type> for PyValueType {
|
||||
fn from(ty: i_slint_compiler::langtype::Type) -> Self {
|
||||
match ty {
|
||||
i_slint_compiler::langtype::Type::Bool => PyValueType::Bool,
|
||||
i_slint_compiler::langtype::Type::Void => PyValueType::Void,
|
||||
i_slint_compiler::langtype::Type::Float32
|
||||
| i_slint_compiler::langtype::Type::Int32
|
||||
| i_slint_compiler::langtype::Type::Duration
|
||||
| i_slint_compiler::langtype::Type::Angle
|
||||
| i_slint_compiler::langtype::Type::PhysicalLength
|
||||
| i_slint_compiler::langtype::Type::LogicalLength
|
||||
| i_slint_compiler::langtype::Type::Percent
|
||||
| i_slint_compiler::langtype::Type::UnitProduct(_) => PyValueType::Number,
|
||||
i_slint_compiler::langtype::Type::String => PyValueType::String,
|
||||
i_slint_compiler::langtype::Type::Array(..) => PyValueType::Model,
|
||||
i_slint_compiler::langtype::Type::Struct { .. } => PyValueType::Struct,
|
||||
i_slint_compiler::langtype::Type::Brush => PyValueType::Brush,
|
||||
i_slint_compiler::langtype::Type::Color => PyValueType::Brush,
|
||||
i_slint_compiler::langtype::Type::Image => PyValueType::Image,
|
||||
i_slint_compiler::langtype::Type::Enumeration(..) => PyValueType::Enumeration,
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
@ -291,22 +329,27 @@ pub struct ComponentInstance {
|
|||
instance: slint_interpreter::ComponentInstance,
|
||||
callbacks: GcVisibleCallbacks,
|
||||
global_callbacks: HashMap<String, GcVisibleCallbacks>,
|
||||
type_collection: TypeCollection,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl ComponentInstance {
|
||||
#[getter]
|
||||
fn definition(&self) -> ComponentDefinition {
|
||||
ComponentDefinition { definition: self.instance.definition() }
|
||||
ComponentDefinition {
|
||||
definition: self.instance.definition(),
|
||||
type_collection: self.type_collection.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_property(&self, name: &str) -> Result<SlintToPyValue, PyGetPropertyError> {
|
||||
Ok(self.instance.get_property(name)?.into())
|
||||
Ok(self.type_collection.to_py_value(self.instance.get_property(name)?))
|
||||
}
|
||||
|
||||
fn set_property(&self, name: &str, value: Bound<'_, PyAny>) -> PyResult<()> {
|
||||
let pv: PyToSlintValue = value.extract()?;
|
||||
Ok(self.instance.set_property(name, pv.0).map_err(|e| PySetPropertyError(e))?)
|
||||
let pv =
|
||||
TypeCollection::slint_value_from_py_value_bound(&value, Some(&self.type_collection))?;
|
||||
Ok(self.instance.set_property(name, pv).map_err(|e| PySetPropertyError(e))?)
|
||||
}
|
||||
|
||||
fn get_global_property(
|
||||
|
@ -314,7 +357,9 @@ impl ComponentInstance {
|
|||
global_name: &str,
|
||||
prop_name: &str,
|
||||
) -> Result<SlintToPyValue, PyGetPropertyError> {
|
||||
Ok(self.instance.get_global_property(global_name, prop_name)?.into())
|
||||
Ok(self
|
||||
.type_collection
|
||||
.to_py_value(self.instance.get_global_property(global_name, prop_name)?))
|
||||
}
|
||||
|
||||
fn set_global_property(
|
||||
|
@ -323,10 +368,11 @@ impl ComponentInstance {
|
|||
prop_name: &str,
|
||||
value: Bound<'_, PyAny>,
|
||||
) -> PyResult<()> {
|
||||
let pv: PyToSlintValue = value.extract()?;
|
||||
let pv =
|
||||
TypeCollection::slint_value_from_py_value_bound(&value, Some(&self.type_collection))?;
|
||||
Ok(self
|
||||
.instance
|
||||
.set_global_property(global_name, prop_name, pv.0)
|
||||
.set_global_property(global_name, prop_name, pv)
|
||||
.map_err(|e| PySetPropertyError(e))?)
|
||||
}
|
||||
|
||||
|
@ -334,10 +380,13 @@ impl ComponentInstance {
|
|||
fn invoke(&self, callback_name: &str, args: Bound<'_, PyTuple>) -> PyResult<SlintToPyValue> {
|
||||
let mut rust_args = vec![];
|
||||
for arg in args.iter() {
|
||||
let pv: PyToSlintValue = arg.extract()?;
|
||||
rust_args.push(pv.0)
|
||||
let pv =
|
||||
TypeCollection::slint_value_from_py_value_bound(&arg, Some(&self.type_collection))?;
|
||||
rust_args.push(pv)
|
||||
}
|
||||
Ok(self.instance.invoke(callback_name, &rust_args).map_err(|e| PyInvokeError(e))?.into())
|
||||
Ok(self.type_collection.to_py_value(
|
||||
self.instance.invoke(callback_name, &rust_args).map_err(|e| PyInvokeError(e))?,
|
||||
))
|
||||
}
|
||||
|
||||
#[pyo3(signature = (global_name, callback_name, *args))]
|
||||
|
@ -349,14 +398,15 @@ impl ComponentInstance {
|
|||
) -> PyResult<SlintToPyValue> {
|
||||
let mut rust_args = vec![];
|
||||
for arg in args.iter() {
|
||||
let pv: PyToSlintValue = arg.extract()?;
|
||||
rust_args.push(pv.0)
|
||||
let pv =
|
||||
TypeCollection::slint_value_from_py_value_bound(&arg, Some(&self.type_collection))?;
|
||||
rust_args.push(pv)
|
||||
}
|
||||
Ok(self
|
||||
.instance
|
||||
.invoke_global(global_name, callback_name, &rust_args)
|
||||
.map_err(|e| PyInvokeError(e))?
|
||||
.into())
|
||||
Ok(self.type_collection.to_py_value(
|
||||
self.instance
|
||||
.invoke_global(global_name, callback_name, &rust_args)
|
||||
.map_err(|e| PyInvokeError(e))?,
|
||||
))
|
||||
}
|
||||
|
||||
fn set_callback(&self, name: &str, callable: PyObject) -> Result<(), PySetCallbackError> {
|
||||
|
@ -373,7 +423,10 @@ impl ComponentInstance {
|
|||
let rust_cb = self
|
||||
.global_callbacks
|
||||
.entry(global_name.to_string())
|
||||
.or_default()
|
||||
.or_insert_with(|| GcVisibleCallbacks {
|
||||
callables: Default::default(),
|
||||
type_collection: self.type_collection.clone(),
|
||||
})
|
||||
.register(callback_name.to_string(), callable);
|
||||
Ok(self.instance.set_global_callback(global_name, callback_name, rust_cb)?.into())
|
||||
}
|
||||
|
@ -404,9 +457,9 @@ impl ComponentInstance {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GcVisibleCallbacks {
|
||||
callables: Rc<RefCell<HashMap<String, PyObject>>>,
|
||||
type_collection: TypeCollection,
|
||||
}
|
||||
|
||||
impl GcVisibleCallbacks {
|
||||
|
@ -414,13 +467,15 @@ impl GcVisibleCallbacks {
|
|||
self.callables.borrow_mut().insert(name.clone(), callable);
|
||||
|
||||
let callables = self.callables.clone();
|
||||
let type_collection = self.type_collection.clone();
|
||||
|
||||
move |args| {
|
||||
let callables = callables.borrow();
|
||||
let callable = callables.get(&name).unwrap();
|
||||
Python::with_gil(|py| {
|
||||
let py_args =
|
||||
PyTuple::new(py, args.iter().map(|v| SlintToPyValue(v.clone()))).unwrap();
|
||||
PyTuple::new(py, args.iter().map(|v| type_collection.to_py_value(v.clone())))
|
||||
.unwrap();
|
||||
let result = match callable.call(py, py_args, None) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
|
@ -430,14 +485,19 @@ impl GcVisibleCallbacks {
|
|||
return Value::Void;
|
||||
}
|
||||
};
|
||||
let pv: PyToSlintValue = match result.extract(py) {
|
||||
|
||||
let pv = match TypeCollection::slint_value_from_py_value(
|
||||
py,
|
||||
&result,
|
||||
Some(&type_collection),
|
||||
) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
eprintln!("Python: Unable to convert return value of Python callback for {name} to Slint value: {err}");
|
||||
return Value::Void;
|
||||
}
|
||||
};
|
||||
pv.0
|
||||
pv
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,24 @@ use pyo3::gc::PyVisit;
|
|||
use pyo3::prelude::*;
|
||||
use pyo3::PyTraverseError;
|
||||
|
||||
use crate::value::{PyToSlintValue, SlintToPyValue};
|
||||
use crate::value::{SlintToPyValue, TypeCollection};
|
||||
|
||||
pub struct PyModelShared {
|
||||
notify: ModelNotify,
|
||||
self_ref: RefCell<Option<PyObject>>,
|
||||
/// The type collection is needed when calling a Python implementation of set_row_data and
|
||||
/// the model data provided (for example from within a .slint file) contains an enum. Then
|
||||
/// we need to know how to map it to the correct Python enum. This field is lazily set, whenever
|
||||
/// time the Python model is exposed to Slint.
|
||||
type_collection: RefCell<Option<TypeCollection>>,
|
||||
}
|
||||
|
||||
impl PyModelShared {
|
||||
pub fn apply_type_collection(&self, type_collection: &TypeCollection) {
|
||||
if let Ok(mut type_collection_ref) = self.type_collection.try_borrow_mut() {
|
||||
*type_collection_ref = Some(type_collection.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -38,6 +51,7 @@ impl PyModelBase {
|
|||
inner: Rc::new(PyModelShared {
|
||||
notify: Default::default(),
|
||||
self_ref: RefCell::new(None),
|
||||
type_collection: RefCell::new(None),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -119,8 +133,12 @@ impl i_slint_core::model::Model for PyModelShared {
|
|||
}
|
||||
};
|
||||
|
||||
match result.extract::<PyToSlintValue>(py) {
|
||||
Ok(pv) => Some(pv.0),
|
||||
match TypeCollection::slint_value_from_py_value(
|
||||
py,
|
||||
&result,
|
||||
self.type_collection.borrow().as_ref(),
|
||||
) {
|
||||
Ok(pv) => Some(pv),
|
||||
Err(err) => {
|
||||
eprintln!("Python: Model implementation of row_data() returned value that cannot be converted to Rust: {err}");
|
||||
None
|
||||
|
@ -137,8 +155,15 @@ impl i_slint_core::model::Model for PyModelShared {
|
|||
return;
|
||||
};
|
||||
|
||||
let Some(type_collection) = self.type_collection.borrow().as_ref().cloned() else {
|
||||
eprintln!(
|
||||
"Python: Model implementation is lacking type collection (in set_row_data)"
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(err) =
|
||||
obj.call_method1(py, "set_row_data", (row, SlintToPyValue::from(data)))
|
||||
obj.call_method1(py, "set_row_data", (row, type_collection.to_py_value(data)))
|
||||
{
|
||||
eprintln!(
|
||||
"Python: Model implementation of set_row_data() threw an exception: {err}"
|
||||
|
@ -168,16 +193,19 @@ impl PyModelShared {
|
|||
}
|
||||
|
||||
#[pyclass(unsendable)]
|
||||
pub struct ReadOnlyRustModel(pub ModelRc<slint_interpreter::Value>);
|
||||
pub struct ReadOnlyRustModel {
|
||||
pub model: ModelRc<slint_interpreter::Value>,
|
||||
pub type_collection: TypeCollection,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl ReadOnlyRustModel {
|
||||
fn row_count(&self) -> usize {
|
||||
self.0.row_count()
|
||||
self.model.row_count()
|
||||
}
|
||||
|
||||
fn row_data(&self, row: usize) -> Option<SlintToPyValue> {
|
||||
self.0.row_data(row).map(|value| value.into())
|
||||
self.model.row_data(row).map(|value| self.type_collection.to_py_value(value))
|
||||
}
|
||||
|
||||
fn __len__(&self) -> usize {
|
||||
|
@ -185,7 +213,11 @@ impl ReadOnlyRustModel {
|
|||
}
|
||||
|
||||
fn __iter__(slf: PyRef<'_, Self>) -> ReadOnlyRustModelIterator {
|
||||
ReadOnlyRustModelIterator { model: slf.0.clone(), row: 0 }
|
||||
ReadOnlyRustModelIterator {
|
||||
model: slf.model.clone(),
|
||||
row: 0,
|
||||
type_collection: slf.type_collection.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn __getitem__(&self, index: usize) -> Option<SlintToPyValue> {
|
||||
|
@ -193,16 +225,11 @@ impl ReadOnlyRustModel {
|
|||
}
|
||||
}
|
||||
|
||||
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,
|
||||
type_collection: TypeCollection,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
|
@ -217,6 +244,6 @@ impl ReadOnlyRustModelIterator {
|
|||
}
|
||||
let row = self.row;
|
||||
self.row += 1;
|
||||
self.model.row_data(row).map(|value| value.into())
|
||||
self.model.row_data(row).map(|value| self.type_collection.to_py_value(value))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -300,11 +300,17 @@ def load_file(
|
|||
|
||||
setattr(module, comp_name, wrapper_class)
|
||||
|
||||
for name, struct_or_enum_prototype in result.structs_and_enums.items():
|
||||
structs, enums = result.structs_and_enums
|
||||
|
||||
for name, struct_prototype in structs.items():
|
||||
name = _normalize_prop(name)
|
||||
struct_wrapper = _build_struct(name, struct_or_enum_prototype)
|
||||
struct_wrapper = _build_struct(name, struct_prototype)
|
||||
setattr(module, name, struct_wrapper)
|
||||
|
||||
for name, enum_class in enums.items():
|
||||
name = _normalize_prop(name)
|
||||
setattr(module, name, enum_class)
|
||||
|
||||
for orig_name, new_name in result.named_exports:
|
||||
orig_name = _normalize_prop(orig_name)
|
||||
new_name = _normalize_prop(new_name)
|
||||
|
|
|
@ -171,7 +171,7 @@ class CompilationResult:
|
|||
component_names: list[str]
|
||||
diagnostics: list[PyDiagnostic]
|
||||
named_exports: list[typing.Tuple[str, str]]
|
||||
structs_and_enums: typing.Dict[str, PyStruct]
|
||||
structs_and_enums: typing.Tuple[typing.Dict[str, PyStruct], typing.Dict[str, Enum]]
|
||||
def component(self, name: str) -> ComponentDefinition: ...
|
||||
|
||||
class Compiler:
|
||||
|
|
|
@ -24,6 +24,11 @@ struct Secret-Struct {
|
|||
|
||||
export { Secret-Struct as Public-Struct }
|
||||
|
||||
export enum TestEnum {
|
||||
Variant1,
|
||||
Variant2,
|
||||
}
|
||||
|
||||
export component App inherits Window {
|
||||
in-out property <string> hello: "World";
|
||||
callback say-hello(string) -> string;
|
||||
|
@ -33,21 +38,21 @@ export component App inherits Window {
|
|||
invoke-say-hello(arg) => {
|
||||
return self.say-hello(arg);
|
||||
}
|
||||
|
||||
callback invoke-say-hello-again(string) -> string;
|
||||
invoke-say-hello-again(arg) => {
|
||||
return self.say-hello-again(arg);
|
||||
}
|
||||
|
||||
callback invoke-global-callback(string) -> string;
|
||||
invoke-global-callback(arg) => {
|
||||
return MyGlobal.global-callback(arg);
|
||||
}
|
||||
|
||||
public function plus-one(value: int) {
|
||||
return value + 1;
|
||||
}
|
||||
|
||||
in-out property <TestEnum> enum-property: TestEnum.Variant2;
|
||||
in-out property <[TestEnum]> model-with-enums: [TestEnum.Variant2, TestEnum.Variant1];
|
||||
|
||||
Rectangle {
|
||||
color: red;
|
||||
}
|
||||
|
|
49
api/python/slint/tests/test_enums.py
Normal file
49
api/python/slint/tests/test_enums.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
# Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||
|
||||
import pytest
|
||||
from slint import load_file, ListModel
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def base_dir() -> Path:
|
||||
origin = __spec__.origin
|
||||
assert origin is not None
|
||||
base_dir = Path(origin).parent
|
||||
assert base_dir is not None
|
||||
return base_dir
|
||||
|
||||
|
||||
def test_enums() -> None:
|
||||
module = load_file(base_dir() / "test-load-file.slint", quiet=False)
|
||||
|
||||
TestEnum = module.TestEnum
|
||||
|
||||
assert TestEnum.Variant1.name == "Variant1"
|
||||
assert TestEnum.Variant1.value == "Variant1"
|
||||
assert TestEnum.Variant2.name == "Variant2"
|
||||
assert TestEnum.Variant2.value == "Variant2"
|
||||
with pytest.raises(
|
||||
AttributeError, match="type object 'TestEnum' has no attribute 'Variant3'"
|
||||
):
|
||||
TestEnum.Variant3
|
||||
|
||||
instance = module.App()
|
||||
assert instance.enum_property == TestEnum.Variant2
|
||||
assert instance.enum_property.__class__ is TestEnum
|
||||
instance.enum_property = TestEnum.Variant1
|
||||
assert instance.enum_property == TestEnum.Variant1
|
||||
assert instance.enum_property.__class__ is TestEnum
|
||||
|
||||
model_with_enums = instance.model_with_enums
|
||||
assert len(model_with_enums) == 2
|
||||
assert model_with_enums[0] == TestEnum.Variant2
|
||||
assert model_with_enums[1] == TestEnum.Variant1
|
||||
assert model_with_enums[0].__class__ is TestEnum
|
||||
model_with_enums = None
|
||||
|
||||
instance.model_with_enums = ListModel([TestEnum.Variant1, TestEnum.Variant2])
|
||||
assert len(instance.model_with_enums) == 2
|
||||
assert instance.model_with_enums[0] == TestEnum.Variant1
|
||||
assert instance.model_with_enums[1] == TestEnum.Variant2
|
||||
assert instance.model_with_enums[0].__class__ is TestEnum
|
|
@ -22,13 +22,14 @@ def test_load_file(caplog: pytest.LogCaptureFixture) -> None:
|
|||
in caplog.text
|
||||
)
|
||||
|
||||
assert len(list(module.__dict__.keys())) == 6
|
||||
assert len(list(module.__dict__.keys())) == 7
|
||||
assert "App" in module.__dict__
|
||||
assert "Diag" in module.__dict__
|
||||
assert "MyDiag" in module.__dict__
|
||||
assert "MyData" in module.__dict__
|
||||
assert "Secret_Struct" in module.__dict__
|
||||
assert "Public_Struct" in module.__dict__
|
||||
assert "TestEnum" in module.__dict__
|
||||
instance = module.App()
|
||||
del instance
|
||||
instance = module.MyDiag()
|
||||
|
|
|
@ -6,13 +6,19 @@ use pyo3::types::PyDict;
|
|||
use pyo3::IntoPyObjectExt;
|
||||
use pyo3_stub_gen::{derive::gen_stub_pyclass, derive::gen_stub_pymethods};
|
||||
|
||||
use std::cell::OnceCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use i_slint_compiler::langtype::Type;
|
||||
|
||||
use i_slint_core::model::{Model, ModelRc};
|
||||
|
||||
#[gen_stub_pyclass]
|
||||
pub struct PyToSlintValue(pub slint_interpreter::Value);
|
||||
|
||||
#[gen_stub_pyclass]
|
||||
pub struct SlintToPyValue(pub slint_interpreter::Value);
|
||||
pub struct SlintToPyValue {
|
||||
pub slint_value: slint_interpreter::Value,
|
||||
pub type_collection: TypeCollection,
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for SlintToPyValue {
|
||||
type Target = PyAny;
|
||||
|
@ -20,7 +26,8 @@ impl<'py> IntoPyObject<'py> for SlintToPyValue {
|
|||
type Error = PyErr;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
match self.0 {
|
||||
let type_collection = self.type_collection;
|
||||
match self.slint_value {
|
||||
slint_interpreter::Value::Void => ().into_bound_py_any(py),
|
||||
slint_interpreter::Value::Number(num) => num.into_bound_py_any(py),
|
||||
slint_interpreter::Value::String(str) => str.into_bound_py_any(py),
|
||||
|
@ -30,16 +37,19 @@ impl<'py> IntoPyObject<'py> for SlintToPyValue {
|
|||
}
|
||||
slint_interpreter::Value::Model(model) => {
|
||||
crate::models::PyModelShared::rust_into_py_model(&model, py).map_or_else(
|
||||
|| crate::models::ReadOnlyRustModel::from(&model).into_bound_py_any(py),
|
||||
|| type_collection.model_to_py(&model).into_bound_py_any(py),
|
||||
|m| Ok(m),
|
||||
)
|
||||
}
|
||||
slint_interpreter::Value::Struct(structval) => {
|
||||
PyStruct { data: structval }.into_bound_py_any(py)
|
||||
type_collection.struct_to_py(structval).into_bound_py_any(py)
|
||||
}
|
||||
slint_interpreter::Value::Brush(brush) => {
|
||||
crate::brush::PyBrush::from(brush).into_bound_py_any(py)
|
||||
}
|
||||
slint_interpreter::Value::EnumerationValue(enum_name, enum_value) => {
|
||||
type_collection.enum_to_py(&enum_name, &enum_value, py)?.into_bound_py_any(py)
|
||||
}
|
||||
v @ _ => {
|
||||
eprintln!("Python: conversion from slint to python needed for {v:#?} and not implemented yet");
|
||||
().into_bound_py_any(py)
|
||||
|
@ -48,10 +58,175 @@ impl<'py> IntoPyObject<'py> for SlintToPyValue {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'py> FromPyObject<'py> for PyToSlintValue {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
#[gen_stub_pyclass]
|
||||
#[pyclass(subclass, unsendable)]
|
||||
#[derive(Clone)]
|
||||
pub struct PyStruct {
|
||||
pub data: slint_interpreter::Struct,
|
||||
pub type_collection: TypeCollection,
|
||||
}
|
||||
|
||||
#[gen_stub_pymethods]
|
||||
#[pymethods]
|
||||
impl PyStruct {
|
||||
fn __getattr__(&self, key: &str) -> PyResult<SlintToPyValue> {
|
||||
self.data.get_field(key).map_or_else(
|
||||
|| {
|
||||
Err(pyo3::exceptions::PyAttributeError::new_err(format!(
|
||||
"Python: No such field {key} on PyStruct"
|
||||
)))
|
||||
},
|
||||
|value| Ok(self.type_collection.to_py_value(value.clone())),
|
||||
)
|
||||
}
|
||||
fn __setattr__(&mut self, py: Python<'_>, key: String, value: PyObject) -> PyResult<()> {
|
||||
let pv =
|
||||
TypeCollection::slint_value_from_py_value(py, &value, Some(&self.type_collection))?;
|
||||
self.data.set_field(key, pv);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn __iter__(slf: PyRef<'_, Self>) -> PyStructFieldIterator {
|
||||
PyStructFieldIterator {
|
||||
inner: slf
|
||||
.data
|
||||
.iter()
|
||||
.map(|(name, val)| (name.to_string(), val.clone()))
|
||||
.collect::<HashMap<_, _>>()
|
||||
.into_iter(),
|
||||
type_collection: slf.type_collection.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn __copy__(&self) -> Self {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[gen_stub_pyclass]
|
||||
#[pyclass(unsendable)]
|
||||
struct PyStructFieldIterator {
|
||||
inner: std::collections::hash_map::IntoIter<String, slint_interpreter::Value>,
|
||||
type_collection: TypeCollection,
|
||||
}
|
||||
|
||||
#[gen_stub_pymethods]
|
||||
#[pymethods]
|
||||
impl PyStructFieldIterator {
|
||||
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
|
||||
slf
|
||||
}
|
||||
|
||||
fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<(String, SlintToPyValue)> {
|
||||
slf.inner.next().map(|(name, val)| (name, slf.type_collection.to_py_value(val)))
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static ENUM_CLASS: OnceCell<PyObject> = OnceCell::new();
|
||||
}
|
||||
|
||||
pub fn enum_class(py: Python) -> PyObject {
|
||||
ENUM_CLASS.with(|cls| {
|
||||
cls.get_or_init(|| -> PyObject {
|
||||
let enum_module = py.import("enum").unwrap();
|
||||
enum_module.getattr("Enum").unwrap().into()
|
||||
})
|
||||
.clone_ref(py)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// Struct that knows about the enums (and maybe other types) exported by
|
||||
/// a `.slint` file loaded with load_file. This is used to map enums
|
||||
/// provided by Slint to the correct python enum classes.
|
||||
pub struct TypeCollection {
|
||||
enum_classes: Rc<HashMap<String, PyObject>>,
|
||||
}
|
||||
|
||||
impl TypeCollection {
|
||||
pub fn new(result: &slint_interpreter::CompilationResult, py: Python<'_>) -> Self {
|
||||
let mut enum_classes = HashMap::new();
|
||||
|
||||
let enum_ctor = crate::value::enum_class(py);
|
||||
|
||||
for struct_or_enum in result.structs_and_enums(i_slint_core::InternalToken {}) {
|
||||
match struct_or_enum {
|
||||
Type::Enumeration(en) => {
|
||||
let enum_type = enum_ctor
|
||||
.call(
|
||||
py,
|
||||
(
|
||||
en.name.to_string(),
|
||||
en.values
|
||||
.iter()
|
||||
.map(|val| {
|
||||
let val = val.to_string();
|
||||
(val.clone(), val)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
enum_classes.insert(en.name.to_string(), enum_type);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let enum_classes = Rc::new(enum_classes);
|
||||
Self { enum_classes }
|
||||
}
|
||||
|
||||
pub fn to_py_value(&self, value: slint_interpreter::Value) -> SlintToPyValue {
|
||||
SlintToPyValue { slint_value: value, type_collection: self.clone() }
|
||||
}
|
||||
|
||||
pub fn struct_to_py(&self, s: slint_interpreter::Struct) -> PyStruct {
|
||||
PyStruct { data: s, type_collection: self.clone() }
|
||||
}
|
||||
|
||||
pub fn enum_to_py(
|
||||
&self,
|
||||
enum_name: &str,
|
||||
enum_value: &str,
|
||||
py: Python<'_>,
|
||||
) -> Result<PyObject, PyErr> {
|
||||
let enum_cls = self.enum_classes.get(enum_name).ok_or_else(|| {
|
||||
PyErr::new::<pyo3::exceptions::PyTypeError, _>(format!(
|
||||
"Slint provided enum {enum_name} is unknown"
|
||||
))
|
||||
})?;
|
||||
enum_cls.getattr(py, enum_value)
|
||||
}
|
||||
|
||||
pub fn model_to_py(
|
||||
&self,
|
||||
model: &ModelRc<slint_interpreter::Value>,
|
||||
) -> crate::models::ReadOnlyRustModel {
|
||||
crate::models::ReadOnlyRustModel { model: model.clone(), type_collection: self.clone() }
|
||||
}
|
||||
|
||||
pub fn enums(&self) -> impl Iterator<Item = (&String, &PyObject)> {
|
||||
self.enum_classes.iter()
|
||||
}
|
||||
|
||||
pub fn slint_value_from_py_value(
|
||||
py: Python<'_>,
|
||||
ob: &PyObject,
|
||||
type_collection: Option<&Self>,
|
||||
) -> PyResult<slint_interpreter::Value> {
|
||||
Self::slint_value_from_py_value_bound(&ob.bind(py), type_collection)
|
||||
}
|
||||
|
||||
pub fn slint_value_from_py_value_bound(
|
||||
ob: &Bound<'_, PyAny>,
|
||||
type_collection: Option<&Self>,
|
||||
) -> PyResult<slint_interpreter::Value> {
|
||||
if ob.is_none() {
|
||||
return Ok(Self(slint_interpreter::Value::Void));
|
||||
return Ok(slint_interpreter::Value::Void);
|
||||
}
|
||||
|
||||
let interpreter_val = ob
|
||||
|
@ -74,26 +249,53 @@ impl<'py> FromPyObject<'py> for PyToSlintValue {
|
|||
.map(|pycolor| slint_interpreter::Value::Brush(pycolor.color.clone().into()))
|
||||
})
|
||||
.or_else(|_| {
|
||||
ob.extract::<PyRef<'_, crate::models::PyModelBase>>()
|
||||
.map(|pymodel| slint_interpreter::Value::Model(pymodel.as_model()))
|
||||
ob.extract::<PyRef<'_, crate::models::PyModelBase>>().map(|pymodel| {
|
||||
slint_interpreter::Value::Model(Self::apply(
|
||||
type_collection,
|
||||
pymodel.as_model(),
|
||||
))
|
||||
})
|
||||
})
|
||||
.or_else(|_| {
|
||||
ob.extract::<PyRef<'_, crate::models::ReadOnlyRustModel>>()
|
||||
.map(|rustmodel| slint_interpreter::Value::Model(rustmodel.0.clone()))
|
||||
ob.extract::<PyRef<'_, crate::models::ReadOnlyRustModel>>().map(|rustmodel| {
|
||||
slint_interpreter::Value::Model(Self::apply(
|
||||
type_collection,
|
||||
rustmodel.model.clone(),
|
||||
))
|
||||
})
|
||||
})
|
||||
.or_else(|_| {
|
||||
ob.extract::<PyRef<'_, PyStruct>>().and_then(|pystruct| {
|
||||
Ok(slint_interpreter::Value::Struct(pystruct.data.clone()))
|
||||
})
|
||||
})
|
||||
.or_else(|_| {
|
||||
ob.is_instance(&enum_class(ob.py()).into_bound(ob.py())).and_then(|r| {
|
||||
r.then(|| {
|
||||
let enum_name =
|
||||
ob.getattr("__class__").and_then(|cls| cls.getattr("__name__"))?;
|
||||
let enum_value = ob.getattr("name")?;
|
||||
Ok(slint_interpreter::Value::EnumerationValue(
|
||||
enum_name.to_string(),
|
||||
enum_value.to_string(),
|
||||
))
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
|
||||
"Object to convert is not an enum",
|
||||
))
|
||||
})
|
||||
})
|
||||
})
|
||||
.or_else(|_| {
|
||||
let dict = ob.downcast::<PyDict>()?;
|
||||
let dict_items: Result<Vec<(String, slint_interpreter::Value)>, PyErr> = dict
|
||||
.iter()
|
||||
.map(|(name, pyval)| {
|
||||
let name = name.extract::<&str>()?.to_string();
|
||||
let slintval = PyToSlintValue::extract_bound(&pyval)?;
|
||||
Ok((name, slintval.0))
|
||||
let slintval =
|
||||
Self::slint_value_from_py_value_bound(&pyval, type_collection)?;
|
||||
Ok((name, slintval))
|
||||
})
|
||||
.collect::<Result<Vec<(_, _)>, PyErr>>();
|
||||
Ok::<_, PyErr>(slint_interpreter::Value::Struct(
|
||||
|
@ -101,83 +303,19 @@ impl<'py> FromPyObject<'py> for PyToSlintValue {
|
|||
))
|
||||
})?;
|
||||
|
||||
Ok(PyToSlintValue(interpreter_val))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<slint_interpreter::Value> for SlintToPyValue {
|
||||
fn from(value: slint_interpreter::Value) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[gen_stub_pyclass]
|
||||
#[pyclass(subclass, unsendable)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct PyStruct {
|
||||
data: slint_interpreter::Struct,
|
||||
}
|
||||
|
||||
#[gen_stub_pymethods]
|
||||
#[pymethods]
|
||||
impl PyStruct {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Default::default()
|
||||
Ok(interpreter_val)
|
||||
}
|
||||
|
||||
fn __getattr__(&self, key: &str) -> PyResult<SlintToPyValue> {
|
||||
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: PyToSlintValue = 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 apply(
|
||||
type_collection: Option<&Self>,
|
||||
model: ModelRc<slint_interpreter::Value>,
|
||||
) -> ModelRc<slint_interpreter::Value> {
|
||||
let Some(type_collection) = type_collection else {
|
||||
return model;
|
||||
};
|
||||
if let Some(rust_model) = model.as_any().downcast_ref::<crate::models::PyModelShared>() {
|
||||
rust_model.apply_type_collection(type_collection);
|
||||
}
|
||||
}
|
||||
|
||||
fn __copy__(&self) -> Self {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<slint_interpreter::Struct> for PyStruct {
|
||||
fn from(data: slint_interpreter::Struct) -> Self {
|
||||
Self { data }
|
||||
}
|
||||
}
|
||||
|
||||
#[gen_stub_pyclass]
|
||||
#[pyclass(unsendable)]
|
||||
struct PyStructFieldIterator {
|
||||
inner: std::collections::hash_map::IntoIter<String, slint_interpreter::Value>,
|
||||
}
|
||||
|
||||
#[gen_stub_pymethods]
|
||||
#[pymethods]
|
||||
impl PyStructFieldIterator {
|
||||
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
|
||||
slf
|
||||
}
|
||||
|
||||
fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<(String, SlintToPyValue)> {
|
||||
slf.inner.next().map(|(name, val)| (name, SlintToPyValue(val)))
|
||||
model
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue