Skip to content

Instantly share code, notes, and snippets.

@manzt
Created October 30, 2022 17:24
Show Gist options
  • Save manzt/487f59a7d6d042f7ce395e43536852b5 to your computer and use it in GitHub Desktop.
Save manzt/487f59a7d6d042f7ce395e43536852b5 to your computer and use it in GitHub Desktop.
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"],
)
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