Skip to content

Commit bb402c7

Browse files
committed
Add mouse events
1 parent bb98ecb commit bb402c7

File tree

6 files changed

+122
-30
lines changed

6 files changed

+122
-30
lines changed

docs/source/events.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
Interactions
2+
============
3+
4+
Using built-in events
5+
---------------------
6+
7+
There are currently two built-in mouse events: ``click`` and ``mouse_move``.
8+
9+
.. code:: Python
10+
11+
def handle_mouse_move(x, y):
12+
# Do something
13+
pass
14+
15+
canvas.on_mouse_move(handle_mouse_move)
16+
17+
def handle_click(x, y):
18+
# Do something else
19+
pass
20+
21+
canvas.on_click(handle_click)
22+
23+
.. note::
24+
Please open an issue or a Pull Request if you want more events to be supported by ipycanvas
25+
26+
Using ipyevents
27+
---------------
28+
29+
If built-in events are not enough for your use case, you can use `ipyevents <https://github.com/mwcraig/ipyevents>`_ which provides mouse and keyboard events.
30+
31+
GamePad support
32+
---------------
33+
34+
If you have a GamePad, you can use the game `Controller <https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20List.html#Controller>`_ widget from ipywidgets to get events from it.

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+
events
3334
advanced
3435

3536
.. toctree::

examples/plotting.ipynb

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"\n",
2222
"from ipywidgets import VBox, IntSlider\n",
2323
"\n",
24-
"from ipycanvas import Canvas, hold_canvas"
24+
"from ipycanvas import MultiCanvas, hold_canvas"
2525
]
2626
},
2727
{
@@ -32,13 +32,14 @@
3232
"source": [
3333
"def init_2d_plot(x, y, color=None, scheme=branca.colormap.linear.RdBu_11, canvas=None, canvas_size=(800, 600), padding=0.1):\n",
3434
" if canvas is None:\n",
35-
" canvas = Canvas(size=canvas_size)\n",
35+
" canvas = MultiCanvas(3, size=canvas_size)\n",
3636
" else:\n",
3737
" canvas.size = canvas_size\n",
3838
"\n",
3939
" padding_x = padding * canvas_size[0]\n",
4040
" padding_y = padding * canvas_size[1]\n",
4141
"\n",
42+
" # TODO Fix drawarea max: It should be (canvas.size - padding)\n",
4243
" drawarea = (drawarea_min_x, drawarea_min_y, drawarea_max_x, drawarea_max_y) = (padding_x, padding_y, canvas_size[0] - 2 * padding_x, canvas_size[1] - 2 * padding_y)\n",
4344
"\n",
4445
" min_x, min_y, max_x, max_y = np.min(x), np.min(y), np.max(x), np.max(y)\n",
@@ -121,26 +122,42 @@
121122
"\n",
122123
" with hold_canvas(canvas):\n",
123124
" canvas.clear()\n",
124-
" canvas.save()\n",
125+
" canvas[1].save()\n",
125126
"\n",
126-
" draw_background(canvas, drawarea, unscale_x, unscale_y)\n",
127+
" draw_background(canvas[0], drawarea, unscale_x, unscale_y)\n",
127128
"\n",
128129
" # Draw scatter\n",
129130
" n_marks = min(x.shape[0], y.shape[0], size.shape[0], color.shape[0])\n",
130131
"\n",
131-
" canvas.stroke_style = stroke_color\n",
132+
" canvas[1].stroke_style = stroke_color\n",
132133
"\n",
133134
" for idx in range(n_marks):\n",
134-
" canvas.fill_style = colormap(color[idx])\n",
135+
" canvas[1].fill_style = colormap(color[idx])\n",
135136
" \n",
136137
" mark_x = scale_x(x[idx])\n",
137138
" mark_y = scale_y(y[idx])\n",
138139
" mark_size = size[idx]\n",
139140
"\n",
140-
" canvas.fill_arc(mark_x, mark_y, mark_size, 0, 2 * pi)\n",
141-
" canvas.stroke_arc(mark_x, mark_y, mark_size, 0, 2 * pi)\n",
141+
" canvas[1].fill_arc(mark_x, mark_y, mark_size, 0, 2 * pi)\n",
142+
" canvas[1].stroke_arc(mark_x, mark_y, mark_size, 0, 2 * pi)\n",
142143
"\n",
143-
" canvas.restore()\n",
144+
" canvas[1].restore()\n",
145+
"\n",
146+
" def click_handler(pixel_x, pixel_y):\n",
147+
" unscaled_x = unscale_x(pixel_x)\n",
148+
" unscaled_y = unscale_y(pixel_y)\n",
149+
"\n",
150+
" for idx in range(n_marks):\n",
151+
" mark_x = x[idx]\n",
152+
" mark_y = y[idx]\n",
153+
" mark_size = size[idx]\n",
154+
"\n",
155+
" if (pixel_x > scale_x(mark_x) - mark_size and pixel_x < scale_x(mark_x) + mark_size and\n",
156+
" pixel_y > scale_y(mark_y) - mark_size and pixel_y < scale_y(mark_y) + mark_size):\n",
157+
" canvas[2].fill_style = 'red'\n",
158+
" canvas[2].fill_arc(scale_x(mark_x), scale_y(mark_y), mark_size, 0, 2 * pi)\n",
159+
"\n",
160+
" canvas[2].on_click(click_handler)\n",
144161
"\n",
145162
" return canvas"
146163
]
@@ -156,28 +173,28 @@
156173
"\n",
157174
" with hold_canvas(canvas):\n",
158175
" canvas.clear()\n",
159-
" canvas.save()\n",
176+
" canvas[1].save()\n",
160177
"\n",
161-
" draw_background(canvas, drawarea, unscale_x, unscale_y)\n",
178+
" draw_background(canvas[0], drawarea, unscale_x, unscale_y)\n",
162179
"\n",
163180
" # Draw lines\n",
164181
" n_points = min(x.shape[0], y.shape[0])\n",
165182
"\n",
166-
" canvas.begin_path()\n",
167-
" canvas.stroke_style = line_color\n",
168-
" canvas.line_width = line_width\n",
169-
" canvas.line_join = 'bevel'\n",
170-
" canvas.line_cap = 'round'\n",
171-
" canvas.move_to(scale_x(x[0]), scale_y(y[0]))\n",
183+
" canvas[1].begin_path()\n",
184+
" canvas[1].stroke_style = line_color\n",
185+
" canvas[1].line_width = line_width\n",
186+
" canvas[1].line_join = 'bevel'\n",
187+
" canvas[1].line_cap = 'round'\n",
188+
" canvas[1].move_to(scale_x(x[0]), scale_y(y[0]))\n",
172189
" for idx in range(1, n_points):\n",
173-
" canvas.line_to(\n",
190+
" canvas[1].line_to(\n",
174191
" scale_x(x[idx]), scale_y(y[idx])\n",
175192
" )\n",
176193
"\n",
177-
" canvas.stroke()\n",
178-
" canvas.close_path()\n",
194+
" canvas[1].stroke()\n",
195+
" canvas[1].close_path()\n",
179196
" \n",
180-
" canvas.restore()\n",
197+
" canvas[1].restore()\n",
181198
"\n",
182199
" return canvas"
183200
]
@@ -196,16 +213,16 @@
196213
"\n",
197214
" with hold_canvas(canvas):\n",
198215
" canvas.clear()\n",
199-
" canvas.save()\n",
216+
" canvas[1].save()\n",
200217
"\n",
201-
" draw_background(canvas, drawarea, unscale_x, unscale_y)\n",
218+
" draw_background(canvas[0], drawarea, unscale_x, unscale_y)\n",
202219
"\n",
203220
" # Draw heatmap\n",
204221
" n_marks = min(x.shape[0], y.shape[0])\n",
205222
"\n",
206223
" for x_idx in range(1, color.shape[0] - 1):\n",
207224
" for y_idx in range(1, color.shape[1] - 1):\n",
208-
" canvas.fill_style = colormap(color[x_idx][y_idx])\n",
225+
" canvas[1].fill_style = colormap(color[x_idx][y_idx])\n",
209226
"\n",
210227
" rect_center = (scale_x(x[x_idx]), scale_y(y[y_idx]))\n",
211228
" neighbours_x = (scale_x(x[x_idx - 1]), scale_x(x[x_idx + 1]))\n",
@@ -217,12 +234,12 @@
217234
" width = rect_low_right_corner[0] - rect_top_left_corner[0]\n",
218235
" height = rect_low_right_corner[1] - rect_top_left_corner[1]\n",
219236
"\n",
220-
" canvas.fill_rect(\n",
237+
" canvas[1].fill_rect(\n",
221238
" rect_top_left_corner[0], rect_top_left_corner[1],\n",
222239
" width, height\n",
223240
" )\n",
224241
"\n",
225-
" canvas.restore()\n",
242+
" canvas[1].restore()\n",
226243
"\n",
227244
" return canvas"
228245
]
@@ -243,6 +260,13 @@
243260
"n_points = 1_000"
244261
]
245262
},
263+
{
264+
"cell_type": "markdown",
265+
"metadata": {},
266+
"source": [
267+
"### Scatter marks are clickable! Try clicking on them"
268+
]
269+
},
246270
{
247271
"cell_type": "code",
248272
"execution_count": null,
@@ -271,9 +295,9 @@
271295
"metadata": {},
272296
"outputs": [],
273297
"source": [
274-
"plot.stroke_style = 'red'\n",
275-
"plot.line_width = 2\n",
276-
"plot.stroke_rect(200, 300, 50, 100)"
298+
"plot[1].stroke_style = 'red'\n",
299+
"plot[1].line_width = 2\n",
300+
"plot[1].stroke_rect(200, 300, 50, 100)"
277301
]
278302
},
279303
{

examples/sprites.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@
215215
"name": "python",
216216
"nbconvert_exporter": "python",
217217
"pygments_lexer": "ipython3",
218-
"version": "3.7.4"
218+
"version": "3.7.3"
219219
}
220220
},
221221
"nbformat": 4,

ipycanvas/canvas.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ class Canvas(DOMWidget):
108108
line_dash_offset = Float(0.)
109109

110110
_client_ready_callbacks = Instance(CallbackDispatcher, ())
111+
_mouse_move_callbacks = Instance(CallbackDispatcher, ())
112+
_click_callbacks = Instance(CallbackDispatcher, ())
111113

112114
def __init__(self, *args, **kwargs):
113115
"""Create a Canvas widget."""
@@ -439,6 +441,14 @@ def on_client_ready(self, callback, remove=False):
439441
"""
440442
self._client_ready_callbacks.register_callback(callback, remove=remove)
441443

444+
def on_mouse_move(self, callback, remove=False):
445+
"""Register a callback that will be called on mouse mouse_move."""
446+
self._mouse_move_callbacks.register_callback(callback, remove=remove)
447+
448+
def on_click(self, callback, remove=False):
449+
"""Register a callback that will be called on mouse click."""
450+
self._click_callbacks.register_callback(callback, remove=remove)
451+
442452
def __setattr__(self, name, value):
443453
super(Canvas, self).__setattr__(name, value)
444454

@@ -472,6 +482,10 @@ def _send_command(self, command, buffers=[]):
472482
def _handle_frontend_event(self, _, content, buffers):
473483
if content.get('event', '') == 'client_ready':
474484
self._client_ready_callbacks()
485+
if content.get('event', '') == 'mouse_move':
486+
self._mouse_move_callbacks(content['x'], content['y'])
487+
if content.get('event', '') == 'click':
488+
self._click_callbacks(content['x'], content['y'])
475489

476490

477491
class MultiCanvas(DOMWidget):

src/widget.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,9 @@ class CanvasView extends DOMWidgetView {
296296
this.resizeCanvas();
297297
this.model.on('change:size', this.resizeCanvas.bind(this));
298298

299+
this.canvas.addEventListener('click', { handleEvent: this.onMouseDown.bind(this) });
300+
this.canvas.addEventListener('mousemove', { handleEvent: this.onMouseMove.bind(this) });
301+
299302
this.updateCanvas();
300303
}
301304

@@ -314,6 +317,22 @@ class CanvasView extends DOMWidgetView {
314317
this.canvas.setAttribute('height', size[1]);
315318
}
316319

320+
private onMouseDown(event: MouseEvent) {
321+
this.model.send({ event: 'click', ...this.getMouseCoordinate(event) }, {});
322+
}
323+
324+
private onMouseMove(event: MouseEvent) {
325+
this.model.send({ event: 'mouse_move', ...this.getMouseCoordinate(event) }, {});
326+
}
327+
328+
private getMouseCoordinate(event: MouseEvent) {
329+
const rect = this.canvas.getBoundingClientRect();
330+
const x = event.clientX - rect.left;
331+
const y = event.clientY - rect.top;
332+
333+
return { x, y };
334+
}
335+
317336
canvas: HTMLCanvasElement;
318337
ctx: CanvasRenderingContext2D;
319338

0 commit comments

Comments
 (0)