Skip to content

Commit 3e04c5a

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 62834a2 commit 3e04c5a

2 files changed

Lines changed: 68 additions & 36 deletions

File tree

lib/matplotlib/widgets.py

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

116-
def __init__(self, ax):
124+
def __init__(self, ax, useblit=False):
117125
self.ax = ax
118126
self._cids = []
119127
self._blit_background_id = None
128+
self._useblit = useblit
120129

121130
def __del__(self):
122131
if self._blit_background_id is not None:
@@ -149,6 +158,34 @@ def _set_cursor(self, cursor):
149158
"""Update the canvas cursor."""
150159
self.ax.get_figure(root=True).canvas.set_cursor(cursor)
151160

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

242279
if image is not None:
243280
ax.imshow(image)
@@ -246,8 +283,6 @@ def __init__(self, ax, label, image=None,
246283
horizontalalignment='center',
247284
transform=ax.transAxes)
248285

249-
self._useblit = useblit
250-
251286
self._observers = cbook.CallbackRegistry(signals=["clicked"])
252287

253288
self.connect_event('button_press_event', self._click)
@@ -283,7 +318,7 @@ def _motion(self, event):
283318
if not colors.same_color(c, self.ax.get_facecolor()):
284319
self.ax.set_facecolor(c)
285320
if self.drawon:
286-
if self._useblit and self.canvas.supports_blit:
321+
if self._should_use_blit():
287322
self.ax.draw_artist(self.ax)
288323
self.canvas.blit(self.ax.bbox)
289324
else:
@@ -1050,14 +1085,12 @@ class _Buttons(AxesWidget):
10501085
"""
10511086

10521087
def __init__(self, ax, labels, *, useblit=True, label_props=None, **kwargs):
1053-
super().__init__(ax)
1088+
super().__init__(ax, useblit=useblit)
10541089

10551090
ax.set_xticks([])
10561091
ax.set_yticks([])
10571092
ax.set_navigate(False)
10581093

1059-
self._useblit = useblit
1060-
10611094
self._buttons_ys = np.linspace(1, 0, len(labels)+2)[1:-1]
10621095

10631096
label_props = _expand_text_props(label_props)
@@ -1072,7 +1105,7 @@ def __init__(self, ax, labels, *, useblit=True, label_props=None, **kwargs):
10721105
self._init_props(text_size, **kwargs)
10731106

10741107
self.connect_event('button_press_event', self._clicked)
1075-
if self._useblit:
1108+
if self._may_use_blit():
10761109
self.connect_event('draw_event', self._clear)
10771110

10781111
self._observers = cbook.CallbackRegistry(signals=["clicked"])
@@ -1084,7 +1117,7 @@ def _clear(self, event):
10841117
"""Internal event handler to clear the buttons."""
10851118
if self.ignore(event) or self.canvas.is_saving():
10861119
return
1087-
if self._useblit and self.canvas.supports_blit:
1120+
if self._should_use_blit():
10881121
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
10891122
self.ax.draw_artist(self._buttons)
10901123

@@ -1320,7 +1353,7 @@ def set_active(self, index, state=None):
13201353
self._buttons.set_facecolor(facecolors)
13211354

13221355
if self.drawon:
1323-
if self._useblit and self.canvas.supports_blit:
1356+
if self._should_use_blit():
13241357
background = self._load_blit_background()
13251358
if background is not None:
13261359
self.canvas.restore_region(background)
@@ -1822,7 +1855,7 @@ def set_active(self, index):
18221855
self._buttons.set_facecolor(button_facecolors)
18231856

18241857
if self.drawon:
1825-
if self._useblit and self.canvas.supports_blit:
1858+
if self._should_use_blit():
18261859
background = self._load_blit_background()
18271860
if background is not None:
18281861
self.canvas.restore_region(background)
@@ -1941,7 +1974,7 @@ class Cursor(AxesWidget):
19411974
"""
19421975
def __init__(self, ax, *, horizOn=True, vertOn=True, useblit=False,
19431976
**lineprops):
1944-
super().__init__(ax)
1977+
super().__init__(ax, useblit=useblit)
19451978

19461979
self.connect_event('motion_notify_event', self.onmove)
19471980
self.connect_event('draw_event', self.clear)
@@ -1962,7 +1995,7 @@ def clear(self, event):
19621995
"""Internal event handler to clear the cursor."""
19631996
if self.ignore(event) or self.canvas.is_saving():
19641997
return
1965-
if self.useblit:
1998+
if self._should_use_blit():
19661999
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
19672000

19682001
@_call_with_reparented_event
@@ -1987,7 +2020,7 @@ def onmove(self, event):
19872020
if not (self.visible and (self.vertOn or self.horizOn)):
19882021
return
19892022
# Redraw.
1990-
if self.useblit:
2023+
if self._should_use_blit():
19912024
background = self._load_blit_background()
19922025
if background is not None:
19932026
self.canvas.restore_region(background)
@@ -2167,14 +2200,13 @@ class _SelectorWidget(AxesWidget):
21672200

21682201
def __init__(self, ax, onselect=None, useblit=False, button=None,
21692202
state_modifier_keys=None, use_data_coordinates=False):
2170-
super().__init__(ax)
2203+
super().__init__(ax, useblit=useblit)
21712204

21722205
self._visible = True
21732206
if onselect is None:
21742207
self.onselect = lambda *args: None
21752208
else:
21762209
self.onselect = onselect
2177-
self._useblit = useblit
21782210
self.connect_default_events()
21792211

21802212
self._state_modifier_keys = dict(move=' ', clear='escape',
@@ -2201,7 +2233,7 @@ def __init__(self, ax, onselect=None, useblit=False, button=None,
22012233
@property
22022234
def useblit(self):
22032235
"""Return whether blitting is used (requested and supported by canvas)."""
2204-
return self._useblit and self.canvas.supports_blit
2236+
return self._should_use_blit()
22052237

22062238
def set_active(self, active):
22072239
super().set_active(active)
@@ -2224,7 +2256,7 @@ def update_background(self, event):
22242256
"""Force an update of the background."""
22252257
# If you add a call to `ignore` here, you'll want to check edge case:
22262258
# `release` can call a draw event even when `ignore` is True.
2227-
if not self.useblit:
2259+
if not self._should_use_blit():
22282260
return
22292261
if self.canvas.is_saving():
22302262
return # saving does not use blitting
@@ -2285,11 +2317,11 @@ def ignore(self, event):
22852317
event.button != self._eventpress.button)
22862318

22872319
def update(self):
2288-
"""Draw using blit() or draw_idle(), depending on ``self.useblit``."""
2320+
"""Draw using blit() or draw_idle(), depending on blitting support."""
22892321
if (not self.ax.get_visible() or
22902322
self.ax.get_figure(root=True)._get_renderer() is None):
22912323
return
2292-
if self.useblit:
2324+
if self._should_use_blit():
22932325
background = self._load_blit_background()
22942326
if background is not None:
22952327
self.canvas.restore_region(background)
@@ -2462,7 +2494,7 @@ def set_props(self, **props):
24622494
artist = self._selection_artist
24632495
props = cbook.normalize_kwargs(props, artist)
24642496
artist.set(**props)
2465-
if self.useblit:
2497+
if self._should_use_blit():
24662498
self.update()
24672499

24682500
def set_handle_props(self, **handle_props):
@@ -2478,7 +2510,7 @@ def set_handle_props(self, **handle_props):
24782510
handle_props = cbook.normalize_kwargs(handle_props, artist)
24792511
for handle in self._handles_artists:
24802512
handle.set(**handle_props)
2481-
if self.useblit:
2513+
if self._should_use_blit():
24822514
self.update()
24832515
self._handle_props.update(handle_props)
24842516

@@ -2641,7 +2673,7 @@ def __init__(self, ax, onselect, direction, *, minspan=0, useblit=False,
26412673
# This relies on the current behavior that the request for
26422674
# useblit is fixed during initialization and cannot be changed
26432675
# afterwards.
2644-
props['animated'] = self._useblit
2676+
props['animated'] = self._may_use_blit()
26452677

26462678
self.direction = direction
26472679
self._extents_on_press = None
@@ -2707,7 +2739,7 @@ def _setup_edge_handles(self, props):
27072739
self._edge_handles = ToolLineHandles(self.ax, positions,
27082740
direction=self.direction,
27092741
line_props=props,
2710-
useblit=self._useblit)
2742+
useblit=self._may_use_blit())
27112743

27122744
@property
27132745
def _handles_artists(self):
@@ -3281,7 +3313,7 @@ def __init__(self, ax, onselect=None, *, minspanx=0,
32813313
if props is None:
32823314
props = dict(facecolor='red', edgecolor='black',
32833315
alpha=0.2, fill=True)
3284-
props = {**props, 'animated': self._useblit}
3316+
props = {**props, 'animated': self._may_use_blit()}
32853317
self._visible = props.pop('visible', self._visible)
32863318
to_draw = self._init_shape(**props)
32873319
self.ax.add_patch(to_draw)
@@ -3306,18 +3338,18 @@ def __init__(self, ax, onselect=None, *, minspanx=0,
33063338
xc, yc = self.corners
33073339
self._corner_handles = ToolHandles(self.ax, xc, yc,
33083340
marker_props=self._handle_props,
3309-
useblit=self._useblit)
3341+
useblit=self._may_use_blit())
33103342

33113343
self._edge_order = ['W', 'S', 'E', 'N']
33123344
xe, ye = self.edge_centers
33133345
self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s',
33143346
marker_props=self._handle_props,
3315-
useblit=self._useblit)
3347+
useblit=self._may_use_blit())
33163348

33173349
xc, yc = self.center
33183350
self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s',
33193351
marker_props=self._handle_props,
3320-
useblit=self._useblit)
3352+
useblit=self._may_use_blit())
33213353

33223354
self._active_handle = None
33233355

@@ -3822,9 +3854,7 @@ def __init__(self, ax, onselect=None, *, useblit=True, props=None, button=None):
38223854
self.verts = None
38233855
props = {
38243856
**(props if props is not None else {}),
3825-
# Note that self.useblit may be != useblit, if the canvas doesn't
3826-
# support blitting.
3827-
'animated': self._useblit, 'visible': False,
3857+
'animated': self._may_use_blit(), 'visible': False,
38283858
}
38293859
line = Line2D([], [], **props)
38303860
self.ax.add_line(line)
@@ -3949,7 +3979,7 @@ def __init__(self, ax, onselect=None, *, useblit=False,
39493979

39503980
if props is None:
39513981
props = dict(color='k', linestyle='-', linewidth=2, alpha=0.5)
3952-
props = {**props, 'animated': self._useblit}
3982+
props = {**props, 'animated': self._may_use_blit()}
39533983
self._selection_artist = line = Line2D([], [], **props)
39543984
self.ax.add_line(line)
39553985

@@ -3958,7 +3988,7 @@ def __init__(self, ax, onselect=None, *, useblit=False,
39583988
markerfacecolor=props.get('color', 'k'))
39593989
self._handle_props = handle_props
39603990
self._polygon_handles = ToolHandles(self.ax, [], [],
3961-
useblit=self._useblit,
3991+
useblit=self._may_use_blit(),
39623992
marker_props=self._handle_props)
39633993

39643994
self._active_handle_idx = -1
@@ -3978,7 +4008,7 @@ def _get_bbox(self):
39784008

39794009
def _add_box(self):
39804010
self._box = RectangleSelector(self.ax,
3981-
useblit=self._useblit,
4011+
useblit=self._may_use_blit(),
39824012
grab_range=self.grab_range,
39834013
handle_props=self._box_handle_props,
39844014
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)