Created
October 30, 2022 17:24
-
-
Save manzt/487f59a7d6d042f7ce395e43536852b5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from setuptools import setup | |
VERSION = "0.1" | |
setup( | |
name="tweakpane", | |
description="Python bindings for Tweakpane UI", | |
author="Trevor Manz", | |
license="Apache License, Version 2.0", | |
version=VERSION, | |
py_modules=["tweakpane"], | |
install_requires=["anywidget"], | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import anywidget | |
import traitlets | |
import contextlib | |
ESM = """ | |
import * as Tweakpane from "https://esm.sh/tweakpane@3"; | |
function addInput(pane, PARAMS, name, options, view) { | |
pane.addInput(PARAMS, name, options).on("change", (e) => { | |
// tweakpane mutates PARAMS in place, so e.value is a reference | |
// to the original object. We need to clone objects so that | |
// Jupyter actually emits a change on `model.set` | |
let value = typeof e.value === "object" | |
? structuredClone(e.value) | |
: e.value; | |
view.model.set(name, value, view); | |
view.model.save_changes(); | |
}); | |
view.listenTo(view.model, `change:${name}`, (_model, value, context) => { | |
// only need to refresh if change comes from other JS views or Python | |
if (context?.cid === view.cid) { | |
return; | |
} | |
PARAMS[name] = value; | |
pane.refresh(); | |
}); | |
} | |
function addMonitor(pane, PARAMS, name, options, view) { | |
pane.addMonitor(PARAMS, name, options); | |
view.listenTo(view.model, `change:${name}`, (_, value, ctx) => { | |
if (ctx?.cid === view.cid) { | |
return; | |
} | |
PARAMS[name] = value; | |
}); | |
} | |
function addAll(pane, PARAMS, inputs, view) { | |
for (let [type, ...rest] of inputs) { | |
if (type === "input" ) { | |
let [name, options] = rest; | |
addInput(pane, PARAMS, name, options, view); | |
continue; | |
} | |
if (type === "monitor" ) { | |
let [name, options] = rest; | |
addMonitor(pane, PARAMS, name, options, view); | |
continue; | |
} | |
if (type === "folder") { | |
let [folderOptions, inputs] = rest; | |
let folder = pane.addFolder(folderOptions); | |
addAll(folder, PARAMS, inputs, view); | |
continue; | |
} | |
throw new Error(`Tweakpane type '${type}' not supported.`); | |
} | |
} | |
function * getNames(inputs) { | |
for (let [type, ...rest] of inputs) { | |
if (type === "input" ) { | |
yield rest[0] | |
continue; | |
} | |
if (type === "monitor" ) { | |
yield rest[0] | |
continue; | |
} | |
if (type === "folder") { | |
yield *getNames(rest[1]) | |
continue; | |
} | |
throw new Error(`Tweakpane type '${type}' not supported.`); | |
} | |
} | |
export function render(view) { | |
function init(inputs) { | |
let pane = new Tweakpane.Pane({ container: view.el }); | |
let PARAMS = {}; | |
for (let name of getNames(inputs)) { | |
PARAMS[name] = view.model.get(name); | |
} | |
addAll(pane, PARAMS, inputs, view); | |
return () => { | |
pane.dispose(); | |
view.stopListening(view.model); | |
}; | |
} | |
let dispose = init(view.model.get("_inputs")); | |
view.model.on("change:_inputs", (_, inputs) => { | |
dispose(); | |
dispose = init(inputs); | |
}); | |
} | |
""" | |
class Pane(anywidget.AnyWidget): | |
_module = traitlets.Unicode(ESM).tag(sync=True) | |
_inputs = traitlets.List([]).tag(sync=True) | |
_context = None | |
def _append_input(self, input): | |
if self._context is None: | |
self._inputs = self._inputs + [input] | |
else: | |
self._context.append(input) | |
def add(self, type, name, value, **options): | |
assert not self.has_trait( | |
name | |
), f"Pane already has trait '{name}'. Choose a new name." | |
t = traitlets.Any(value).tag(sync=True) | |
self.add_traits(**{name: t}) | |
self._append_input((type, name, options)) | |
def add_input(self, name: str, value, **options): | |
self.add("input", name, value, **options) | |
def add_monitor(self, name: str, value, **options): | |
self.add("monitor", name, value, **options) | |
def folder(self, title: str, **options): | |
return folder_context_manager(self, title=title, **options) | |
@contextlib.contextmanager | |
def folder_context_manager(pane: Pane, **options): | |
prev = pane._context | |
pane._context = [] | |
try: | |
yield pane | |
finally: | |
folder_contents = pane._context | |
pane._context = prev | |
pane._append_input(("folder", options, folder_contents)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment