8000 Merge pull request #21790 from greglucas/macosx-draw-refactor · matplotlib/matplotlib@808dbe4 · GitHub
[go: up one dir, main page]

Skip to content

Commit 808dbe4

Browse files
authored
Merge pull request #21790 from greglucas/macosx-draw-refactor
FIX: Update blitting and drawing on the macosx backend
2 parents 396a010 + 5415418 commit 808dbe4

File tree

3 files changed

+125
-27
lines changed

3 files changed

+125
-27
lines changed

lib/matplotlib/backends/backend_macosx.py

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,64 @@ def __init__(self, figure):
3030
FigureCanvasBase.__init__(self, figure)
3131
width, height = self.get_width_height()
3232
_macosx.FigureCanvas.__init__(self, width, height)
33+
self._draw_pending = False
34+
self._is_drawing = False
3335

3436
def set_cursor(self, cursor):
3537
# docstring inherited
3638
_macosx.set_cursor(cursor)
3739

38-
def _draw(self):
39-
renderer = self.get_renderer()
40-
if self.figure.stale:
41-
renderer.clear()
42-
self.figure.draw(renderer)
43-
return renderer
44-
4540
def draw(self):
46-
# docstring inherited
47-
self._draw()
48-
self.flush_events()
41+
"""Render the figure and update the macosx canvas."""
42+
# The renderer draw is done here; delaying causes problems with code
43+
# that uses the result of the draw() to update plot elements.
44+
if self._is_drawing:
45+
return
46+
with cbook._setattr_cm(self, _is_drawing=True):
47+
super().draw()
48+
self.update()
4949

50-
# draw_idle is provided by _macosx.FigureCanvas
50+
def draw_idle(self):
51+
# docstring inherited
52+
if not (getattr(self, '_draw_pending', False) or
53+
getattr(self, '_is_drawing', False)):
54+
self._draw_pending = True
55+
# Add a singleshot timer to the eventloop that will call back
56+
# into the Python method _draw_idle to take care of the draw
57+
self._single_shot_timer(self._draw_idle)
58+
59+
def _single_shot_timer(self, callback):
60+
"""Add a single shot timer with the given callback"""
61+
# We need to explicitly stop (called from delete) the timer after
62+
# firing, otherwise segfaults will occur when trying to deallocate
63+
# the singleshot timers.
64+
def callback_func(callback, timer):
65+
callback()
66+
del timer
67+
timer = self.new_timer(interval=0)
68+
timer.add_callback(callback_func, callback, timer)
69+
timer.start()
70+
71+
def _draw_idle(self):
72+
"""
73+
Draw method for singleshot timer
74+
75+
This draw method can be added to a singleshot timer, which can
76+
accumulate draws while the eventloop is spinning. This method will
77+
then only draw the first time and short-circuit the others.
78+
"""
79+
with self._idle_draw_cntx():
80+
if not self._draw_pending:
81+
# Short-circuit because our draw request has already been
82+
# taken care of
83+
return
84+
self._draw_pending = False
85+
self.draw()
5186

5287
def blit(self, bbox=None):
53-
self.draw_idle()
88+
# docstring inherited
89+
super().blit(bbox)
90+
self.update()
5491

5592
def resize(self, width, height):
5693
# Size from macOS is logical pixels, dpi is physical.

lib/matplotlib/tests/test_backends_interactive.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,3 +413,72 @@ def _lazy_headless():
413413
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
414414
def test_lazy_linux_headless():
415415
proc = _run_helper(_lazy_headless, timeout=_test_timeout, MPLBACKEND="")
416+
417+
418+
def _test_number_of_draws_script():
419+
import matplotlib.pyplot as plt
420+
421+
fig, ax = plt.subplots()
422+
423+
# animated=True tells matplotlib to only draw the artist when we
424+
# explicitly request it
425+
ln, = ax.plot([0, 1], [1, 2], animated=True)
426+
427+
# make sure the window is raised, but the script keeps going
428+
plt.show(block=False)
429+
plt.pause(0.3)
430+
# Connect to draw_event to count the occurrences
431+
fig.canvas.mpl_connect('draw_event', print)
432+
433+
# get copy of entire figure (everything inside fig.bbox)
434+
# sans animated artist
435+
bg = fig.canvas.copy_from_bbox(fig.bbox)
436+
# draw the animated artist, this uses a cached renderer
437+
ax.draw_artist(ln)
438+
# show the result to the screen
439+
fig.canvas.blit(fig.bbox)
440+
441+
for j in range(10):
442+
# reset the background back in the canvas state, screen unchanged
443+
fig.canvas.restore_region(bg)
444+
# Create a **new** artist here, this is poor usage of blitting
445+
# but good for testing to make sure that this doesn't create
446+
# excessive draws
447+
ln, = ax.plot([0, 1], [1, 2])
448+
# render the artist, updating the canvas state, but not the screen
449+
ax.draw_artist(ln)
450+
# copy the image to the GUI state, but screen might not changed yet
451+
fig.canvas.blit(fig.bbox)
452+
# flush any pending GUI events, re-painting the screen if needed
453+
fig.canvas.flush_events()
454+
455+
# Let the event loop process everything before leaving
456+
plt.pause(0.1)
457+
458+
459+
_blit_backends = _get_testable_interactive_backends()
460+
for param in _blit_backends:
461+
backend = param.values[0]["MPLBACKEND"]
462+
if backend == "gtk3cairo":
463+
# copy_from_bbox only works when rendering to an ImageSurface
464+
param.marks.append(
465+
pytest.mark.skip("gtk3cairo does not support blitting"))
466+
elif backend == "wx":
467+
param.marks.append(
468+
pytest.mark.skip("wx does not support blitting"))
469+
470+
471+
@pytest.mark.parametrize("env", _blit_backends)
472+
# subprocesses can struggle to get the display, so rerun a few times
473+
@pytest.mark.flaky(reruns=4)
474+
def test_blitting_events(env):
475+
proc = _run_helper(_test_number_of_draws_script,
476+
timeout=_test_timeout,
477+
**env)
478+
479+
# Count the number of draw_events we got. We could count some initial
480+
# canvas draws (which vary in number by backend), but the critical
481+
# check here is that it isn't 10 draws, which would be called if
482+
# blitting is not properly implemented
483+
ndraws = proc.stdout.count("DrawEvent")
484+
assert 0 < ndraws < 5

src/_macosx.m

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -345,14 +345,7 @@ static CGFloat _get_device_scale(CGContextRef cr)
345345
}
346346

347347
static PyObject*
348-
FigureCanvas_draw(FigureCanvas* self)
349-
{
350-
[self->view display];
351-
Py_RETURN_NONE;
352-
}
353-
354-
static PyObject*
355-
FigureCanvas_draw_idle(FigureCanvas* self)
348+
FigureCanvas_update(FigureCanvas* self)
356349
{
357350
[self->view setNeedsDisplay: YES];
358351
Py_RETURN_NONE;
@@ -361,6 +354,9 @@ static CGFloat _get_device_scale(CGContextRef cr)
361354
static PyObject*
362355
FigureCanvas_flush_events(FigureCanvas* self)
363356
{
357+
// We need to allow the runloop to run very briefly
358+
// to allow the view to be displayed when used in a fast updating animation
359+
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.0]];
364360
[self->view displayIfNeeded];
365361
Py_RETURN_NONE;
366362
}
@@ -485,12 +481,8 @@ static CGFloat _get_device_scale(CGContextRef cr)
485481
.tp_new = (newfunc)FigureCanvas_new,
486482
.tp_doc = "A FigureCanvas object wraps a Cocoa NSView object.",
487483
.tp_methods = (PyMethodDef[]){
488-
{"draw",
489-
(PyCFunction)FigureCanvas_draw,
490-
METH_NOARGS,
491-
NULL}, // docstring inherited
492-
{"draw_idle",
493-
(PyCFunction)FigureCanvas_draw_idle,
484+
{"update",
485+
(PyCFunction)FigureCanvas_update,
494486
METH_NOARGS,
495487
NULL}, // docstring inherited
496488
{"flush_events",
@@ -1263,7 +1255,7 @@ -(void)drawRect:(NSRect)rect
12631255

12641256
CGContextRef cr = [[NSGraphicsContext currentContext] CGContext];
12651257

1266-
if (!(renderer = PyObject_CallMethod(canvas, "_draw", ""))
1258+
if (!(renderer = PyObject_CallMethod(canvas, "get_renderer", ""))
12671259
|| !(renderer_buffer = PyObject_GetAttrString(renderer, "_renderer"))) {
12681260
PyErr_Print();
12691261
goto exit;

0 commit comments

Comments
 (0)
0