|
| 1 | +import asyncio |
| 2 | +from functools import wraps |
| 3 | +from threading import Thread |
| 4 | +from queue import Queue as SyncQueue |
| 5 | + |
| 6 | +import ipywidgets as widgets |
| 7 | +from IPython.display import display as ipython_display |
| 8 | +from traitlets import Unicode |
| 9 | +from idom.core.layout import Layout, LayoutEvent, LayoutUpdate |
| 10 | + |
| 11 | + |
| 12 | +_JUPYTER_SERVER_BASE_URL = "" |
| 13 | + |
| 14 | + |
| 15 | +def set_jupyter_server_base_url(base_url): |
| 16 | + global _JUPYTER_SERVER_BASE_URL |
| 17 | + _JUPYTER_SERVER_BASE_URL = base_url |
| 18 | + |
| 19 | + |
| 20 | +def run(constructor): |
| 21 | + """Run the given IDOM elemen definition as a Jupyter Widget. |
| 22 | + |
| 23 | + This function is meant to be similarly to ``idom.run``. |
| 24 | + """ |
| 25 | + return ipython_display(LayoutWidget(constructor())) |
| 26 | + |
| 27 | + |
| 28 | +def widgetize(constructor): |
| 29 | + """A decorator that turns an IDOM element into a Jupyter Widget constructor""" |
| 30 | + |
| 31 | + @wraps(constructor) |
| 32 | + def wrapper(*args, **kwargs): |
| 33 | + return LayoutWidget(constructor(*args, **kwargs)) |
| 34 | + |
| 35 | + return wrapper |
| 36 | + |
| 37 | + |
| 38 | +@widgets.register |
| 39 | +class LayoutWidget(widgets.DOMWidget): |
| 40 | + """A widget for displaying IDOM elements""" |
| 41 | + |
| 42 | + # Name of the widget view class in front-end |
| 43 | + _view_name = Unicode("IdomView").tag(sync=True) |
| 44 | + |
| 45 | + # Name of the widget model class in front-end |
| 46 | + _model_name = Unicode("IdomModel").tag(sync=True) |
| 47 | + |
| 48 | + # Name of the front-end module containing widget view |
| 49 | + _view_module = Unicode("idom-client-jupyter").tag(sync=True) |
| 50 | + |
| 51 | + # Name of the front-end module containing widget model |
| 52 | + _model_module = Unicode("idom-client-jupyter").tag(sync=True) |
| 53 | + |
| 54 | + # Version of the front-end module containing widget view |
| 55 | + _view_module_version = Unicode("^0.1.0").tag(sync=True) |
| 56 | + # Version of the front-end module containing widget model |
| 57 | + _model_module_version = Unicode("^0.1.0").tag(sync=True) |
| 58 | + |
| 59 | + _jupyter_server_base_url = Unicode().tag(sync=True) |
| 60 | + |
| 61 | + def __init__(self, element): |
| 62 | + super().__init__(_jupyter_server_base_url=_JUPYTER_SERVER_BASE_URL) |
| 63 | + self._idom_model = {} |
| 64 | + self._idom_views = set() |
| 65 | + self._idom_layout = Layout(element) |
| 66 | + self._idom_loop = _spawn_threaded_event_loop(self._idom_layout_render_loop()) |
| 67 | + self.on_msg(self._idom_on_msg) |
| 68 | + |
| 69 | + @staticmethod |
| 70 | + def _idom_on_msg(self, message, buffers): |
| 71 | + m_type = message.get("type") |
| 72 | + if m_type == "client-ready": |
| 73 | + v_id = message["viewID"] |
| 74 | + self._idom_views.add(v_id) |
| 75 | + update = LayoutUpdate.create_from({}, self._idom_model) |
| 76 | + self.send({"viewID": v_id, "data": update}) |
| 77 | + elif m_type == "dom-event": |
| 78 | + asyncio.run_coroutine_threadsafe( |
| 79 | + self._idom_layout.dispatch(LayoutEvent(**message["data"])), |
| 80 | + loop=self._idom_loop, |
| 81 | + ) |
| 82 | + elif m_type == "client-removed": |
| 83 | + v_id = message["viewID"] |
| 84 | + if v_id in self._idom_views: |
| 85 | + self._idom_views.remove(message["viewID"]) |
| 86 | + |
| 87 | + async def _idom_layout_render_loop(self): |
| 88 | + async with self._idom_layout: |
| 89 | + while True: |
| 90 | + update = await self._idom_layout.render() |
| 91 | + |
| 92 | + self._idom_model = update.apply_to(self._idom_model) |
| 93 | + for v_id in self._idom_views: |
| 94 | + self.send({"viewID": v_id, "data": update}) |
| 95 | + |
| 96 | + |
| 97 | +def _spawn_threaded_event_loop(coro): |
| 98 | + loop_q = SyncQueue() |
| 99 | + |
| 100 | + def run_in_thread() -> None: |
| 101 | + loop = asyncio.new_event_loop() |
| 102 | + asyncio.set_event_loop(loop) |
| 103 | + loop_q.put(loop) |
| 104 | + loop.run_until_complete(coro) |
| 105 | + |
| 106 | + thread = Thread(target=run_in_thread, daemon=True) |
| 107 | + thread.start() |
| 108 | + |
| 109 | + return loop_q.get() |
0 commit comments