Skip to content

Commit 3b23b21

Browse files
authored
Merge pull request #42 from martinRenou/client_event
Add client_ready event
2 parents a4dda67 + b230e97 commit 3b23b21

File tree

4 files changed

+60
-1
lines changed

4 files changed

+60
-1
lines changed

docs/source/advanced.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Advanced usage
2+
==============
3+
4+
ipycanvas in Voilà
5+
------------------
6+
7+
The ``Canvas`` is a stateless widget, in that the actual state of the ``Canvas`` (pixel colors, transformation state, etc) is not saved nor synchronized between the Python kernel and the client page. This means that whenever a new client connects to the kernel, it will get a blank canvas and you would need to replay all your drawings.
8+
9+
This is an ipycanvas limitation, but it's also a way to keep it fast (not having to synchronize everything).
10+
11+
This limitation results in ipycanvas not working with `Voilà <https://github.com/voila-dashboards/voila>`_ out of the box. Because the Voilà page connects to the kernel only when the entire Notebook has been executed, all the drawings may already have happened and the canvas end up being blank when it's created.
12+
13+
A way to work around this is to perform your drawings in a callback that gets called when the client is ready to receive drawings, using the ``on_client_ready`` method:
14+
15+
.. code:: Python
16+
17+
from ipycanvas import Canvas
18+
19+
canvas = Canvas(size=(100, 50))
20+
21+
def perform_drawings():
22+
canvas.font = '32px serif'
23+
canvas.fill_text('Voilà!', 10, 32)
24+
25+
canvas.on_client_ready(perform_drawings)
26+
27+
canvas

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ You can try ipycanvas, without the need of installing anything on your computer,
3030
drawing_images
3131
canvas_state
3232
transformations
33+
advanced
3334

3435
.. toctree::
3536
:caption: API Reference

ipycanvas/canvas.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from traitlets import Enum, Float, Instance, List, Tuple, Unicode, observe
1212

13-
from ipywidgets import Color, DOMWidget, widget_serialization
13+
from ipywidgets import CallbackDispatcher, Color, DOMWidget, widget_serialization
1414

1515
from ._frontend import module_name, module_version
1616

@@ -91,6 +91,8 @@ class Canvas(DOMWidget):
9191
#: (float) Specifies where to start a dash array on a line. Default is ``0.``.
9292
line_dash_offset = Float(0.)
9393

94+
_client_ready_callbacks = Instance(CallbackDispatcher, ())
95+
9496
def __init__(self, *args, **kwargs):
9597
"""Create a Canvas widget."""
9698
#: Whether commands should be cached or not
@@ -102,6 +104,8 @@ def __init__(self, *args, **kwargs):
102104
self.layout.width = str(self.size[0]) + 'px'
103105
self.layout.height = str(self.size[1]) + 'px'
104106

107+
self.on_msg(self._handle_frontend_event)
108+
105109
# Rectangles methods
106110
def fill_rect(self, x, y, width, height=None):
107111
"""Draw a filled rectangle of size ``(width, height)`` at the ``(x, y)`` position."""
@@ -396,6 +400,17 @@ def flush(self):
396400
self._commands_cache = []
397401
self._buffers_cache = []
398402

403+
# Events
404+
def on_client_ready(self, callback, remove=False):
405+
"""Register a callback that will be called when a new client is ready to receive draw commands.
406+
407+
When a new client connects to the kernel he will get an empty Canvas (because the canvas is
408+
almost stateless, the new client does not know what draw commands were previously sent). So
409+
this function is useful for replaying your drawing whenever a new client connects and is
410+
ready to receive draw commands.
411+
"""
412+
self._client_ready_callbacks.register_callback(callback, remove=remove)
413+
399414
def __setattr__(self, name, value):
400415
super(Canvas, self).__setattr__(name, value)
401416

@@ -425,6 +440,10 @@ def _send_command(self, command, buffers=[]):
425440
else:
426441
self.send(command, buffers)
427442

443+
def _handle_frontend_event(self, _, content, buffers):
444+
if content.get('event', '') == 'client_ready':
445+
self._client_ready_callbacks()
446+
428447

429448
class MultiCanvas(DOMWidget):
430449
"""Create a MultiCanvas widget with n_canvases Canvas widgets.
@@ -462,6 +481,16 @@ def _on_size_change(self, change):
462481
for canvas in self._canvases:
463482
canvas.size = change.new
464483

484+
def on_client_ready(self, callback, remove=False):
485+
"""Register a callback that will be called when a new client is ready to receive draw commands.
486+
487+
When a new client connects to the kernel he will get an empty Canvas (because the canvas is
488+
almost stateless, the new client does not know what draw commands were previously sent). So
489+
this function is useful for replaying your drawing whenever a new client connects and is
490+
ready to receive draw commands.
491+
"""
492+
self._canvases[-1]._client_ready_callbacks.register_callback(callback, remove=remove)
493+
465494

466495
@contextmanager
467496
def hold_canvas(canvas):

src/widget.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ class CanvasModel extends DOMWidgetModel {
5151

5252
this.on('change:size', this.resizeCanvas.bind(this));
5353
this.on('msg:custom', this.onCommand.bind(this));
54+
55+
this.send({ event: 'client_ready' }, {});
5456
}
5557

5658
private onCommand(command: any, buffers: any) {

0 commit comments

Comments
 (0)