~raph/crochet

818a1e4d45af7b36b377c575030e68002980e06d — Raph Levien 1 year, 4 months ago b873eb1
Add Python module

This adds just button and label, but is scriptable from Python.
4 files changed, 131 insertions(+), 0 deletions(-)

A python/Cargo.toml
A python/README.md
A python/run.py
A python/src/lib.rs
A python/Cargo.toml => python/Cargo.toml +23 -0
@@ 0,0 1,23 @@
[package]
name = "crochet_py"
version = "0.1.0"
license = "Apache-2.0"
authors = ["Raph Levien <raph.levien@gmail.com>"]
categories = ["gui"]
readme = "README.md"
edition = "2018"

[lib]
name = "crochet_py"
crate-type = ["cdylib"]

[dependencies.druid]
git = "https://github.com/linebender/druid"
rev = "7c3646509fffb412d16f5746b8a6c11dbaacd602"

[dependencies.pyo3]
version = "0.12"
features = ["extension-module"]

[dependencies]
crochet = { path = "../" }

A python/README.md => python/README.md +17 -0
@@ 0,0 1,17 @@
# Python bindings

This crate contains the beginnings of Python bindings for Druid with the Crochet architecture.

It uses [PyO3] for the Python integration. Please refer to their documentation for prerequisites and instructions how to get extension modules written in Rust to work with the Python on your machine. However, as a quick start, on Windows (using a mingw shell) the following command should work:

```sh
cargo build && cp target/debug/crochet_py.dll crochet_py.pyd && python run.py
```

Functionality is currently very limited; at the moment, this is really a proof of concept to see whether the integration is possible. To build out more of the Crochet architecture, we envision using [inspect.currentframe()] to give unique caller locations, comparable to `#[track_caller]` in Rust.

Also, it's likely the integration would use explicit `begin` and `end` methods across the language boundary, relying on Python's [`with`] to enforce nesting, rather than running through Rust closures. But these are details to be determined.

[PyO3]: https://github.com/PyO3/pyo3
[inspect.currentframe]: https://docs.python.org/3/library/inspect.html#inspect.currentframe
[`with`]: https://docs.python.org/2.5/whatsnew/pep-343.html

A python/run.py => python/run.py +16 -0
@@ 0,0 1,16 @@
import crochet_py

class MyApp:
    def __init__(self):
        self.count = 0

    def run(self, cx):
        cx.label(f'Current count: {self.count}')
        if cx.button('Increment'):
            self.count += 1
        if self.count > 3:
            cx.label('You did it!')

my_app = MyApp()

crochet_py.pop_up_window(my_app.run)

A python/src/lib.rs => python/src/lib.rs +75 -0
@@ 0,0 1,75 @@
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

use druid::{AppLauncher, Widget, WindowDesc};

use crochet::{self, AppHolder, Button, DruidAppData, Label};

mod safe_ref;

use safe_ref::SafeRef;

struct PyAppLogic {
    py_app: PyObject,
}

impl PyAppLogic {
    fn run(&mut self, cx: &mut crochet::Cx) {
        // We need the transmute here because of the lifetime parameter.
        // The Cx itself is protected.
        let cx = unsafe { std::mem::transmute(cx) };
        Python::with_gil(|py| {
            SafeRef::scoped(py, cx, |cx_ref| {
                let py_cx = Py::new(py, Cx { inner: cx_ref }).unwrap();
                self.py_app.call(py, (py_cx,), None).unwrap();
            });
        });

    }
}

#[pyfunction]
fn pop_up_window(py_app: PyObject) -> PyResult<()> {
    let main_window = WindowDesc::new(|| ui_builder(py_app));
    let data = Default::default();
    AppLauncher::with_window(main_window)
        .use_simple_logger()
        .launch(data)
        .unwrap();
    Ok(())
}

fn ui_builder(py_app: PyObject) -> impl Widget<DruidAppData> {
    let mut app_logic = PyAppLogic { py_app };

    AppHolder::new(move |cx| app_logic.run(cx))
}

#[pyclass]
struct Cx {
    inner: SafeRef<crochet::Cx<'static>>,
}

#[pymethods]
impl Cx {
    fn label(&mut self, py: Python<'_>, text: &str) {
        if let Some(cx) = self.inner.try_get_mut(py) {
            Label::new(text).build(cx);
        }
    }

    fn button(&mut self, py: Python<'_>, text: &str) -> bool {
        if let Some(cx) = self.inner.try_get_mut(py) {
            Button::new(text).build(cx)
        } else {
            false
        }
    }
}

#[pymodule]
fn crochet_py(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(pop_up_window, m)?)?;

    Ok(())
}