Skip to content

Commit 13562e7

Browse files
committed
MNT: Towards standardizing widget blitting
To have standard logical framework for blitting of widgets, we pull some logic up into AxesWidget: - Most widgets held a _useblit variable. This is now part of AxesWidget. - AxesWidget._should_use_blit() returns whether blitting should *currently* be done - i.e. the user/widget requested it and the canvas supports it. This is used to guard all blitting-related code. Note: This is a well-defined and limited step towards standardization. Not all widgets are already using it. Some still have special logic. I anticipate that migrating them will be easier when this infrastructure and logical concept is present. Changes here are limited to obvious cases. The rest will be handled in a follow-up PR.
1 parent ddd4196 commit 13562e7

2 files changed

Lines changed: 69 additions & 39 deletions

File tree

lib/matplotlib/widgets.py

Lines changed: 66 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,21 @@ class AxesWidget(Widget):
113113
The parent figure canvas for the widget.
114114
active : bool
115115
If False, the widget does not respond to events.
116+
useblit : bool
117+
Whether usage of blitting is desired. The actual usage of
118+
blitting also depends on the canvas supporting it.
119+
120+
Once set, this is read-only, as some widgets currently still
121+
depend on the state statically. They still query _useblit.
122+
123+
.. versionadded:: 3.11
116124
"""
117125

118-
def __init__(self, ax):
126+
def __init__(self, ax, useblit=False):
119127
self.ax = ax
120128
self._cids = []
121129
self._blit_background_id = None
130+
self._useblit = useblit
122131

123132
def __del__(self):
124133
if self._blit_background_id is not None:
@@ -151,6 +160,34 @@ def _set_cursor(self, cursor):
151160
"""Update the canvas cursor."""
152161
self.ax.get_figure(root=True).canvas.set_cursor(cursor)
153162

163+
def _may_use_blit(self):
164+
"""
165+
Return whether blitting could potentially be used.
166+
167+
This is defined by the *useblit* parameter upon initialization
168+
and currently cannot be changed afterwards. Widgets can
169+
set up differently depending on this value. In particular they
170+
can create Artists with ``animated=self._may_use_blit()``.
171+
This makes sure these Artists properly work with blitting if
172+
that is applied, but is also safe to use if blitting is not
173+
applied.
174+
175+
Note: We define this separately from _useblit for semantic
176+
clarity. Eventually, we want to migrate widget code away from
177+
directly accessing _useblit.
178+
"""
179+
return self._useblit
180+
181+
def _should_use_blit(self):
182+
"""
183+
Return whether blitting should be used.
184+
185+
All blitting-related code must be guarded by this because
186+
not all canvases support blit and canvases may be swapped
187+
out during the lifetime of the widget.
188+
"""
189+
return self._useblit and self.canvas.supports_blit
190+
154191
def _save_blit_background(self, background):
155192
"""
156193
Save a blit background.
@@ -239,7 +276,7 @@ def __init__(self, ax, label, image=None,
239276
240277
.. versionadded:: 3.7
241278
"""
242-
super().__init__(ax)
279+
super().__init__(ax, useblit=useblit)
243280

244281
if image is not None:
245282
ax.imshow(image)
@@ -248,8 +285,6 @@ def __init__(self, ax, label, image=None,
248285
horizontalalignment='center',
249286
transform=ax.transAxes)
250287

251-
self._useblit = useblit
252-
253288
self._observers = cbook.CallbackRegistry(signals=["clicked"])
254289

255290
self.connect_event('button_press_event', self._click)
@@ -285,7 +320,7 @@ def _motion(self, event):
285320
if not colors.same_color(c, self.ax.get_facecolor()):
286321
self.ax.set_facecolor(c)
287322
if self.drawon:
288-
if self._useblit and self.canvas.supports_blit:
323+
if self._should_use_blit():
289324
self.ax.draw_artist(self.ax)
290325
self.canvas.blit(self.ax.bbox)
291326
else:
@@ -1096,7 +1131,7 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
10961131
10971132
.. versionadded:: 3.7
10981133
"""
1099-
super().__init__(ax)
1134+
super().__init__(ax, useblit=useblit)
11001135

11011136
_api.check_isinstance((dict, None), label_props=label_props,
11021137
frame_props=frame_props, check_props=check_props)
@@ -1108,8 +1143,6 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
11081143
if actives is None:
11091144
actives = [False] * len(labels)
11101145

1111-
self._useblit = useblit
1112-
11131146
ys = np.linspace(1, 0, len(labels)+2)[1:-1]
11141147

11151148
label_props = _expand_text_props(label_props)
@@ -1158,7 +1191,7 @@ def _clear(self, event):
11581191
"""Internal event handler to clear the buttons."""
11591192
if self.ignore(event) or self.canvas.is_saving():
11601193
return
1161-
if self._useblit and self.canvas.supports_blit:
1194+
if self._should_use_blit():
11621195
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
11631196
self.ax.draw_artist(self._checks)
11641197

@@ -1264,7 +1297,7 @@ def set_active(self, index, state=None):
12641297
self._checks.set_facecolor(facecolors)
12651298

12661299
if self.drawon:
1267-
if self._useblit and self.canvas.supports_blit:
1300+
if self._should_use_blit():
12681301
background = self._load_blit_background()
12691302
if background is not None:
12701303
self.canvas.restore_region(background)
@@ -1678,7 +1711,7 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16781711
16791712
.. versionadded:: 3.7
16801713
"""
1681-
super().__init__(ax)
1714+
super().__init__(ax, useblit=useblit)
16821715

16831716
_api.check_isinstance((dict, None), label_props=label_props,
16841717
radio_props=radio_props)
@@ -1705,8 +1738,6 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
17051738

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

1708-
self._useblit = useblit
1709-
17101741
label_props = _expand_text_props(label_props)
17111742
self.labels = [
17121743
ax.text(0.25, y, label, transform=ax.transAxes,
@@ -1751,7 +1782,7 @@ def _clear(self, event):
17511782
"""Internal event handler to clear the buttons."""
17521783
if self.ignore(event) or self.canvas.is_saving():
17531784
return
1754-
if self._useblit and self.canvas.supports_blit:
1785+
if self._should_use_blit():
17551786
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
17561787
self.ax.draw_artist(self._buttons)
17571788

@@ -1845,7 +1876,7 @@ def set_active(self, index):
18451876
self._buttons.set_facecolor(button_facecolors)
18461877

18471878
if self.drawon:
1848-
if self._useblit and self.canvas.supports_blit:
1879+
if self._should_use_blit():
18491880
background = self._load_blit_background()
18501881
if background is not None:
18511882
self.canvas.restore_region(background)
@@ -1989,7 +2020,7 @@ class Cursor(AxesWidget):
19892020
"""
19902021
def __init__(self, ax, *, horizOn=True, vertOn=True, useblit=False,
19912022
**lineprops):
1992-
super().__init__(ax)
2023+
super().__init__(ax, useblit=useblit)
19932024

19942025
self.connect_event('motion_notify_event', self.onmove)
19952026
self.connect_event('draw_event', self.clear)
@@ -2010,7 +2041,7 @@ def clear(self, event):
20102041
"""Internal event handler to clear the cursor."""
20112042
if self.ignore(event) or self.canvas.is_saving():
20122043
return
2013-
if self.useblit:
2044+
if self._should_use_blit():
20142045
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
20152046

20162047
@_call_with_reparented_event
@@ -2035,7 +2066,7 @@ def onmove(self, event):
20352066
if not (self.visible and (self.vertOn or self.horizOn)):
20362067
return
20372068
# Redraw.
2038-
if self.useblit:
2069+
if self._should_use_blit():
20392070
background = self._load_blit_background()
20402071
if background is not None:
20412072
self.canvas.restore_region(background)
@@ -2205,14 +2236,13 @@ class _SelectorWidget(AxesWidget):
22052236

22062237
def __init__(self, ax, onselect=None, useblit=False, button=None,
22072238
state_modifier_keys=None, use_data_coordinates=False):
2208-
super().__init__(ax)
2239+
super().__init__(ax, useblit=useblit)
22092240

22102241
self._visible = True
22112242
if onselect is None:
22122243
self.onselect = lambda *args: None
22132244
else:
22142245
self.onselect = onselect
2215-
self._useblit = useblit
22162246
self.connect_default_events()
22172247

22182248
self._state_modifier_keys = dict(move=' ', clear='escape',
@@ -2239,7 +2269,7 @@ def __init__(self, ax, onselect=None, useblit=False, button=None,
22392269
@property
22402270
def useblit(self):
22412271
"""Return whether blitting is used (requested and supported by canvas)."""
2242-
return self._useblit and self.canvas.supports_blit
2272+
return self._should_use_blit()
22432273

22442274
def set_active(self, active):
22452275
super().set_active(active)
@@ -2262,7 +2292,7 @@ def update_background(self, event):
22622292
"""Force an update of the background."""
22632293
# If you add a call to `ignore` here, you'll want to check edge case:
22642294
# `release` can call a draw event even when `ignore` is True.
2265-
if not self.useblit:
2295+
if not self._should_use_blit():
22662296
return
22672297
if self.canvas.is_saving():
22682298
return # saving does not use blitting
@@ -2323,11 +2353,11 @@ def ignore(self, event):
23232353
event.button != self._eventpress.button)
23242354

23252355
def update(self):
2326-
"""Draw using blit() or draw_idle(), depending on ``self.useblit``."""
2356+
"""Draw using blit() or draw_idle(), depending on blitting support."""
23272357
if (not self.ax.get_visible() or
23282358
self.ax.get_figure(root=True)._get_renderer() is None):
23292359
return
2330-
if self.useblit:
2360+
if self._should_use_blit():
23312361
background = self._load_blit_background()
23322362
if background is not None:
23332363
self.canvas.restore_region(background)
@@ -2500,7 +2530,7 @@ def set_props(self, **props):
25002530
artist = self._selection_artist
25012531
props = cbook.normalize_kwargs(props, artist)
25022532
artist.set(**props)
2503-
if self.useblit:
2533+
if self._should_use_blit():
25042534
self.update()
25052535

25062536
def set_handle_props(self, **handle_props):
@@ -2516,7 +2546,7 @@ def set_handle_props(self, **handle_props):
25162546
handle_props = cbook.normalize_kwargs(handle_props, artist)
25172547
for handle in self._handles_artists:
25182548
handle.set(**handle_props)
2519-
if self.useblit:
2549+
if self._should_use_blit():
25202550
self.update()
25212551
self._handle_props.update(handle_props)
25222552

@@ -2675,7 +2705,7 @@ def __init__(self, ax, onselect, direction, *, minspan=0, useblit=False,
26752705
# This relies on the current behavior that the request for
26762706
# useblit is fixed during initialization and cannot be changed
26772707
# afterwards.
2678-
props['animated'] = self._useblit
2708+
props['animated'] = self._may_use_blit()
26792709

26802710
self.direction = direction
26812711
self._extents_on_press = None
@@ -2741,7 +2771,7 @@ def _setup_edge_handles(self, props):
27412771
self._edge_handles = ToolLineHandles(self.ax, positions,
27422772
direction=self.direction,
27432773
line_props=props,
2744-
useblit=self._useblit)
2774+
useblit=self._may_use_blit())
27452775

27462776
@property
27472777
def _handles_artists(self):
@@ -3315,7 +3345,7 @@ def __init__(self, ax, onselect=None, *, minspanx=0,
33153345
if props is None:
33163346
props = dict(facecolor='red', edgecolor='black',
33173347
alpha=0.2, fill=True)
3318-
props = {**props, 'animated': self._useblit}
3348+
props = {**props, 'animated': self._may_use_blit()}
33193349
self._visible = props.pop('visible', self._visible)
33203350
to_draw = self._init_shape(**props)
33213351
self.ax.add_patch(to_draw)
@@ -3340,18 +3370,18 @@ def __init__(self, ax, onselect=None, *, minspanx=0,
33403370
xc, yc = self.corners
33413371
self._corner_handles = ToolHandles(self.ax, xc, yc,
33423372
marker_props=self._handle_props,
3343-
useblit=self._useblit)
3373+
useblit=self._may_use_blit())
33443374

33453375
self._edge_order = ['W', 'S', 'E', 'N']
33463376
xe, ye = self.edge_centers
33473377
self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s',
33483378
marker_props=self._handle_props,
3349-
useblit=self._useblit)
3379+
useblit=self._may_use_blit())
33503380

33513381
xc, yc = self.center
33523382
self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s',
33533383
marker_props=self._handle_props,
3354-
useblit=self._useblit)
3384+
useblit=self._may_use_blit())
33553385

33563386
self._active_handle = None
33573387

@@ -3856,9 +3886,7 @@ def __init__(self, ax, onselect=None, *, useblit=True, props=None, button=None):
38563886
self.verts = None
38573887
props = {
38583888
**(props if props is not None else {}),
3859-
# Note that self.useblit may be != useblit, if the canvas doesn't
3860-
# support blitting.
3861-
'animated': self._useblit, 'visible': False,
3889+
'animated': self._may_use_blit(), 'visible': False,
38623890
}
38633891
line = Line2D([], [], **props)
38643892
self.ax.add_line(line)
@@ -3983,7 +4011,7 @@ def __init__(self, ax, onselect=None, *, useblit=False,
39834011

39844012
if props is None:
39854013
props = dict(color='k', linestyle='-', linewidth=2, alpha=0.5)
3986-
props = {**props, 'animated': self._useblit}
4014+
props = {**props, 'animated': self._may_use_blit()}
39874015
self._selection_artist = line = Line2D([], [], **props)
39884016
self.ax.add_line(line)
39894017

@@ -3992,7 +4020,7 @@ def __init__(self, ax, onselect=None, *, useblit=False,
39924020
markerfacecolor=props.get('color', 'k'))
39934021
self._handle_props = handle_props
39944022
self._polygon_handles = ToolHandles(self.ax, [], [],
3995-
useblit=self._useblit,
4023+
useblit=self._may_use_blit(),
39964024
marker_props=self._handle_props)
39974025

39984026
self._active_handle_idx = -1
@@ -4012,7 +4040,7 @@ def _get_bbox(self):
40124040

40134041
def _add_box(self):
40144042
self._box = RectangleSelector(self.ax,
4015-
useblit=self._useblit,
4043+
useblit=self._may_use_blit(),
40164044
grab_range=self.grab_range,
40174045
handle_props=self._box_handle_props,
40184046
props=self._box_props,

lib/matplotlib/widgets.pyi

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ class Widget:
3434

3535
class AxesWidget(Widget):
3636
ax: Axes
37-
def __init__(self, ax: Axes) -> None: ...
37+
def __init__(self, ax: Axes, useblit: bool = ...) -> None: ...
3838
def __del__(self) -> None: ...
3939
@property
4040
def canvas(self) -> FigureCanvasBase | None: ...
4141
def connect_event(self, event: Event, callback: Callable) -> None: ...
4242
def disconnect_events(self) -> None: ...
4343
def _set_cursor(self, cursor: Cursors) -> None: ...
44+
def _may_use_blit(self) -> bool: ...
45+
def _should_use_blit(self) -> bool: ...
4446

4547
class Button(AxesWidget):
4648
label: Text

0 commit comments

Comments
 (0)