Skip to content

Commit d364278

Browse files
committed
FIX: Make widget blitting compatible with swapped canvas
1 parent e7e5865 commit d364278

File tree

2 files changed

+69
-17
lines changed

2 files changed

+69
-17
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1740,6 +1740,10 @@ class FigureCanvasBase:
17401740

17411741
filetypes = _default_filetypes
17421742

1743+
# global counter to assign unique ids to blit backgrounds
1744+
# see _get_blit_background_id()
1745+
_last_blit_background_id = 0
1746+
17431747
@_api.classproperty
17441748
def supports_blit(cls):
17451749
"""If this Canvas sub-class supports blitting."""
@@ -1765,6 +1769,7 @@ def __init__(self, figure=None):
17651769
# We don't want to scale up the figure DPI more than once.
17661770
figure._original_dpi = figure.dpi
17671771
self._device_pixel_ratio = 1
1772+
self._blit_backgrounds = {}
17681773
super().__init__() # Typically the GUI widget init (if any).
17691774

17701775
callbacks = property(lambda self: self.figure._canvas_callbacks)
@@ -1840,6 +1845,33 @@ def is_saving(self):
18401845
def blit(self, bbox=None):
18411846
"""Blit the canvas in bbox (default entire canvas)."""
18421847

1848+
@classmethod
1849+
def _get_blit_background_id(cls):
1850+
"""
1851+
Get a globally unique id that can be used to store a blit background.
1852+
1853+
Blitting support is canvas-dependent, so blitting mechanisms should
1854+
store their backgrounds in the canvas, more precisely in
1855+
``canvas._blit_backgrounds[id]``. The id must be obtained via this
1856+
function to ensure it is globally unique.
1857+
1858+
The content of ``canvas._blit_backgrounds[id]`` is not specified.
1859+
We leave this freedom to the blitting mechanism.
1860+
1861+
Blitting mechanisms must not expect that a background that they
1862+
have stored is still there at a later time. The canvas may have
1863+
been switched out, or we may add other mechanisms later that
1864+
invalidate blit backgrounds (e.g. dpi changes).
1865+
Therefore, always query as `_blit_backgrounds.get(id)` and be
1866+
prepared for a None return value.
1867+
1868+
Note: The blit background API is still experimental and may change
1869+
in the future without warning.
1870+
"""
1871+
cls._last_blit_background_id += 1
1872+
return cls._last_blit_background_id
1873+
1874+
18431875
def inaxes(self, xy):
18441876
"""
18451877
Return the topmost visible `~.axes.Axes` containing the point *xy*.

lib/matplotlib/widgets.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ class AxesWidget(Widget):
117117
def __init__(self, ax):
118118
self.ax = ax
119119
self._cids = []
120+
self._blit_background_id = None
120121

121122
canvas = property(
122123
lambda self: getattr(self.ax.get_figure(root=True), 'canvas', None)
@@ -155,6 +156,26 @@ def _set_cursor(self, cursor):
155156
"""Update the canvas cursor."""
156157
self.ax.get_figure(root=True).canvas.set_cursor(cursor)
157158

159+
def _save_blit_background(self, background):
160+
"""
161+
Save a blit background.
162+
163+
The background is stored on the canvas in a uniquely identifiable way.
164+
It should be read back via `._load_blit_background`. Be prepared that
165+
some events may invalidate the background, in which case
166+
`._load_blit_background` will return None.
167+
168+
This currently allows at most one background per widget, which is
169+
good enough for all existing widgets.
170+
"""
171+
if self._blit_background_id is None:
172+
self._blit_background_id = self.canvas._get_blit_background_id()
173+
self.canvas._blit_backgrounds[self._blit_background_id] = background
174+
175+
def _load_blit_background(self):
176+
"""Load a blit background; may be None at any time."""
177+
return self.canvas._blit_backgrounds.get(self._blit_background_id)
178+
158179

159180
class Button(AxesWidget):
160181
"""
@@ -1063,7 +1084,6 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
10631084
actives = [False] * len(labels)
10641085

10651086
self._useblit = useblit and self.canvas.supports_blit
1066-
self._background = None
10671087

10681088
ys = np.linspace(1, 0, len(labels)+2)[1:-1]
10691089

@@ -1110,7 +1130,7 @@ def _clear(self, event):
11101130
"""Internal event handler to clear the buttons."""
11111131
if self.ignore(event) or self.canvas.is_saving():
11121132
return
1113-
self._background = self.canvas.copy_from_bbox(self.ax.bbox)
1133+
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
11141134
self.ax.draw_artist(self._checks)
11151135

11161136
def _clicked(self, event):
@@ -1215,8 +1235,9 @@ def set_active(self, index, state=None):
12151235

12161236
if self.drawon:
12171237
if self._useblit:
1218-
if self._background is not None:
1219-
self.canvas.restore_region(self._background)
1238+
background = self._load_blit_background()
1239+
if background is not None:
1240+
self.canvas.restore_region(background)
12201241
self.ax.draw_artist(self._checks)
12211242
self.canvas.blit(self.ax.bbox)
12221243
else:
@@ -1650,7 +1671,6 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16501671
ys = np.linspace(1, 0, len(labels) + 2)[1:-1]
16511672

16521673
self._useblit = useblit and self.canvas.supports_blit
1653-
self._background = None
16541674

16551675
label_props = _expand_text_props(label_props)
16561676
self.labels = [
@@ -1692,7 +1712,7 @@ def _clear(self, event):
16921712
"""Internal event handler to clear the buttons."""
16931713
if self.ignore(event) or self.canvas.is_saving():
16941714
return
1695-
self._background = self.canvas.copy_from_bbox(self.ax.bbox)
1715+
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
16961716
self.ax.draw_artist(self._buttons)
16971717

16981718
def _clicked(self, event):
@@ -1785,8 +1805,9 @@ def set_active(self, index):
17851805

17861806
if self.drawon:
17871807
if self._useblit:
1788-
if self._background is not None:
1789-
self.canvas.restore_region(self._background)
1808+
background = self._load_blit_background()
1809+
if background is not None:
1810+
self.canvas.restore_region(background)
17901811
self.ax.draw_artist(self._buttons)
17911812
self.canvas.blit(self.ax.bbox)
17921813
else:
@@ -1942,15 +1963,14 @@ def __init__(self, ax, *, horizOn=True, vertOn=True, useblit=False,
19421963
self.lineh = ax.axhline(ax.get_ybound()[0], visible=False, **lineprops)
19431964
self.linev = ax.axvline(ax.get_xbound()[0], visible=False, **lineprops)
19441965

1945-
self.background = None
19461966
self.needclear = False
19471967

19481968
def clear(self, event):
19491969
"""Internal event handler to clear the cursor."""
19501970
if self.ignore(event) or self.canvas.is_saving():
19511971
return
19521972
if self.useblit:
1953-
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
1973+
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
19541974

19551975
def onmove(self, event):
19561976
"""Internal event handler to draw the cursor when the mouse moves."""
@@ -1975,8 +1995,9 @@ def onmove(self, event):
19751995
return
19761996
# Redraw.
19771997
if self.useblit:
1978-
if self.background is not None:
1979-
self.canvas.restore_region(self.background)
1998+
background = self._load_blit_background()
1999+
if background is not None:
2000+
self.canvas.restore_region(background)
19802001
self.ax.draw_artist(self.linev)
19812002
self.ax.draw_artist(self.lineh)
19822003
self.canvas.blit(self.ax.bbox)
@@ -2137,8 +2158,6 @@ def __init__(self, ax, onselect=None, useblit=False, button=None,
21372158
self._state_modifier_keys.update(state_modifier_keys or {})
21382159
self._use_data_coordinates = use_data_coordinates
21392160

2140-
self.background = None
2141-
21422161
if isinstance(button, Integral):
21432162
self.validButtons = [button]
21442163
else:
@@ -2194,7 +2213,7 @@ def update_background(self, event):
21942213
for artist in artists:
21952214
stack.enter_context(artist._cm_set(visible=False))
21962215
self.canvas.draw()
2197-
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
2216+
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
21982217
if needs_redraw:
21992218
for artist in artists:
22002219
self.ax.draw_artist(artist)
@@ -2241,8 +2260,9 @@ def update(self):
22412260
self.ax.get_figure(root=True)._get_renderer() is None):
22422261
return
22432262
if self.useblit:
2244-
if self.background is not None:
2245-
self.canvas.restore_region(self.background)
2263+
background = self._load_blit_background()
2264+
if background is not None:
2265+
self.canvas.restore_region(background)
22462266
else:
22472267
self.update_background(None)
22482268
# We need to draw all artists, which are not included in the

0 commit comments

Comments
 (0)