Skip to content

Commit 3edc6c7

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

File tree

3 files changed

+92
-17
lines changed

3 files changed

+92
-17
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 50 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,51 @@ 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+
def _release_blit_background_id(self, bb_id):
1875+
"""
1876+
Release a blit background id that is no longer needed.
1877+
1878+
This removes the respective entry from the internal storage, i.e.
1879+
the ``canvas._blit_backgrounds`` dict, and thus allows to free the
1880+
associated memory.
1881+
1882+
After releasing the id you must not use it anymore.
1883+
1884+
It is safe to release an id that has not been used with the canvas
1885+
or that has already been released.
1886+
1887+
Note: The blit background API is still experimental and may change
1888+
in the future without warning.
1889+
"""
1890+
if bb_id in self._blit_backgrounds:
1891+
del self._blit_backgrounds[bb_id]
1892+
18431893
def inaxes(self, xy):
18441894
"""
18451895
Return the topmost visible `~.axes.Axes` containing the point *xy*.

lib/matplotlib/widgets.py

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ class AxesWidget(Widget):
117117
def __init__(self, ax):
118118
self.ax = ax
119119
self._cids = []
120+
self._blit_background_id = None
121+
122+
def __del__(self):
123+
if self._blit_background_id is not None:
124+
self.canvas._release_blit_background_id(self._blit_background_id)
120125

121126
canvas = property(
122127
lambda self: getattr(self.ax.get_figure(root=True), 'canvas', None)
@@ -155,6 +160,26 @@ def _set_cursor(self, cursor):
155160
"""Update the canvas cursor."""
156161
self.ax.get_figure(root=True).canvas.set_cursor(cursor)
157162

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

159184
class Button(AxesWidget):
160185
"""
@@ -1063,7 +1088,6 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
10631088
actives = [False] * len(labels)
10641089

10651090
self._useblit = useblit and self.canvas.supports_blit
1066-
self._background = None
10671091

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

@@ -1110,7 +1134,7 @@ def _clear(self, event):
11101134
"""Internal event handler to clear the buttons."""
11111135
if self.ignore(event) or self.canvas.is_saving():
11121136
return
1113-
self._background = self.canvas.copy_from_bbox(self.ax.bbox)
1137+
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
11141138
self.ax.draw_artist(self._checks)
11151139

11161140
def _clicked(self, event):
@@ -1215,8 +1239,9 @@ def set_active(self, index, state=None):
12151239

12161240
if self.drawon:
12171241
if self._useblit:
1218-
if self._background is not None:
1219-
self.canvas.restore_region(self._background)
1242+
background = self._load_blit_background()
1243+
if background is not None:
1244+
self.canvas.restore_region(background)
12201245
self.ax.draw_artist(self._checks)
12211246
self.canvas.blit(self.ax.bbox)
12221247
else:
@@ -1650,7 +1675,6 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16501675
ys = np.linspace(1, 0, len(labels) + 2)[1:-1]
16511676

16521677
self._useblit = useblit and self.canvas.supports_blit
1653-
self._background = None
16541678

16551679
label_props = _expand_text_props(label_props)
16561680
self.labels = [
@@ -1692,7 +1716,7 @@ def _clear(self, event):
16921716
"""Internal event handler to clear the buttons."""
16931717
if self.ignore(event) or self.canvas.is_saving():
16941718
return
1695-
self._background = self.canvas.copy_from_bbox(self.ax.bbox)
1719+
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
16961720
self.ax.draw_artist(self._buttons)
16971721

16981722
def _clicked(self, event):
@@ -1785,8 +1809,9 @@ def set_active(self, index):
17851809

17861810
if self.drawon:
17871811
if self._useblit:
1788-
if self._background is not None:
1789-
self.canvas.restore_region(self._background)
1812+
background = self._load_blit_background()
1813+
if background is not None:
1814+
self.canvas.restore_region(background)
17901815
self.ax.draw_artist(self._buttons)
17911816
self.canvas.blit(self.ax.bbox)
17921817
else:
@@ -1942,15 +1967,14 @@ def __init__(self, ax, *, horizOn=True, vertOn=True, useblit=False,
19421967
self.lineh = ax.axhline(ax.get_ybound()[0], visible=False, **lineprops)
19431968
self.linev = ax.axvline(ax.get_xbound()[0], visible=False, **lineprops)
19441969

1945-
self.background = None
19461970
self.needclear = False
19471971

19481972
def clear(self, event):
19491973
"""Internal event handler to clear the cursor."""
19501974
if self.ignore(event) or self.canvas.is_saving():
19511975
return
19521976
if self.useblit:
1953-
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
1977+
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
19541978

19551979
def onmove(self, event):
19561980
"""Internal event handler to draw the cursor when the mouse moves."""
@@ -1975,8 +1999,9 @@ def onmove(self, event):
19751999
return
19762000
# Redraw.
19772001
if self.useblit:
1978-
if self.background is not None:
1979-
self.canvas.restore_region(self.background)
2002+
background = self._load_blit_background()
2003+
if background is not None:
2004+
self.canvas.restore_region(background)
19802005
self.ax.draw_artist(self.linev)
19812006
self.ax.draw_artist(self.lineh)
19822007
self.canvas.blit(self.ax.bbox)
@@ -2137,8 +2162,6 @@ def __init__(self, ax, onselect=None, useblit=False, button=None,
21372162
self._state_modifier_keys.update(state_modifier_keys or {})
21382163
self._use_data_coordinates = use_data_coordinates
21392164

2140-
self.background = None
2141-
21422165
if isinstance(button, Integral):
21432166
self.validButtons = [button]
21442167
else:
@@ -2194,7 +2217,7 @@ def update_background(self, event):
21942217
for artist in artists:
21952218
stack.enter_context(artist._cm_set(visible=False))
21962219
self.canvas.draw()
2197-
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
2220+
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
21982221
if needs_redraw:
21992222
for artist in artists:
22002223
self.ax.draw_artist(artist)
@@ -2241,8 +2264,9 @@ def update(self):
22412264
self.ax.get_figure(root=True)._get_renderer() is None):
22422265
return
22432266
if self.useblit:
2244-
if self.background is not None:
2245-
self.canvas.restore_region(self.background)
2267+
background = self._load_blit_background()
2268+
if background is not None:
2269+
self.canvas.restore_region(background)
22462270
else:
22472271
self.update_background(None)
22482272
# We need to draw all artists, which are not included in the

lib/matplotlib/widgets.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class Widget:
3535
class AxesWidget(Widget):
3636
ax: Axes
3737
def __init__(self, ax: Axes) -> None: ...
38+
def __del__(self) -> None: ...
3839
@property
3940
def canvas(self) -> FigureCanvasBase | None: ...
4041
def connect_event(self, event: Event, callback: Callable) -> None: ...

0 commit comments

Comments
 (0)