Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Improve cursor icons with RectangleSelector
  • Loading branch information
dstansby authored and hasanrashid committed Oct 2, 2025
commit 7935cceb1c897fe2db109d2b15d9e6031bdd1035
56 changes: 43 additions & 13 deletions lib/matplotlib/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ def ignore(self, event):
# docstring inherited
return super().ignore(event) or self.canvas is None

def _set_cursor(self, cursor):
"""Update the canvas cursor."""
self.ax.get_figure(root=True).canvas.set_cursor(cursor)


class Button(AxesWidget):
"""
Expand Down Expand Up @@ -2645,7 +2649,7 @@ def _handles_artists(self):
else:
return ()

def _set_cursor(self, enabled):
def _set_span_cursor(self, enabled):
"""Update the canvas cursor based on direction of the selector."""
if enabled:
cursor = (backend_tools.Cursors.RESIZE_HORIZONTAL
Expand All @@ -2654,7 +2658,7 @@ def _set_cursor(self, enabled):
else:
cursor = backend_tools.Cursors.POINTER

self.ax.get_figure(root=True).canvas.set_cursor(cursor)
self._set_cursor(cursor)

def connect_default_events(self):
# docstring inherited
Expand All @@ -2664,7 +2668,7 @@ def connect_default_events(self):

def _press(self, event):
"""Button press event handler."""
self._set_cursor(True)
self._set_span_cursor(True)
if self._interactive and self._selection_artist.get_visible():
self._set_active_handle(event)
else:
Expand Down Expand Up @@ -2714,7 +2718,7 @@ def direction(self, direction):

def _release(self, event):
"""Button release event handler."""
self._set_cursor(False)
self._set_span_cursor(False)

if not self._interactive:
self._selection_artist.set_visible(False)
Expand Down Expand Up @@ -2756,7 +2760,7 @@ def _hover(self, event):
return

_, e_dist = self._edge_handles.closest(event.x, event.y)
self._set_cursor(e_dist <= self.grab_range)
self._set_span_cursor(e_dist <= self.grab_range)

def _onmove(self, event):
"""Motion notify event handler."""
Expand Down Expand Up @@ -3280,10 +3284,24 @@ def _press(self, event):
self._rotation_on_press = self._rotation
self._set_aspect_ratio_correction()

match self._get_action():
case "rotate":
# TODO: set to a rotate cursor if possible?
pass
case "move":
self._set_cursor(backend_tools.cursors.MOVE)
case "resize":
# TODO: set to a resize cursor if possible?
pass
case "create":
self._set_cursor(backend_tools.cursors.SELECT_REGION)


return False

def _release(self, event):
"""Button release event handler."""
self._set_cursor(backend_tools.Cursors.POINTER)
if not self._interactive:
self._selection_artist.set_visible(False)

Expand Down Expand Up @@ -3327,9 +3345,23 @@ def _release(self, event):
self.update()
self._active_handle = None
self._extents_on_press = None

return False

def _get_action(self):
"""
Return one of "rotate", "move", "resize", "create"
"""
state = self._state
if 'rotate' in state and self._active_handle in self._corner_order:
return 'rotate'
elif self._active_handle == 'C':
return 'move'
elif self._active_handle:
return 'resize'

return 'create'


def _onmove(self, event):
"""
Motion notify event handler.
Expand All @@ -3344,12 +3376,10 @@ def _onmove(self, event):
# The calculations are done for rotation at zero: we apply inverse
# transformation to events except when we rotate and move
state = self._state
rotate = 'rotate' in state and self._active_handle in self._corner_order
move = self._active_handle == 'C'
resize = self._active_handle and not move
action = self._get_action()

xdata, ydata = self._get_data_coords(event)
if resize:
if action == "resize":
inv_tr = self._get_rotation_transform().inverted()
xdata, ydata = inv_tr.transform([xdata, ydata])
eventpress.xdata, eventpress.ydata = inv_tr.transform(
Expand All @@ -3369,7 +3399,7 @@ def _onmove(self, event):

x0, x1, y0, y1 = self._extents_on_press
# rotate an existing shape
if rotate:
if action == "rotate":
# calculate angle abc
a = (eventpress.xdata, eventpress.ydata)
b = self.center
Expand All @@ -3378,7 +3408,7 @@ def _onmove(self, event):
np.arctan2(a[1]-b[1], a[0]-b[0]))
self.rotation = np.rad2deg(self._rotation_on_press + angle)

elif resize:
elif action == "resize":
size_on_press = [x1 - x0, y1 - y0]
center = (x0 + size_on_press[0] / 2, y0 + size_on_press[1] / 2)

Expand Down Expand Up @@ -3429,7 +3459,7 @@ def _onmove(self, event):
sign = np.sign(xdata - x0)
x1 = x0 + sign * abs(y1 - y0) * self._aspect_ratio_correction

elif move:
elif action == "move":
x0, x1, y0, y1 = self._extents_on_press
dx = xdata - eventpress.xdata
dy = ydata - eventpress.ydata
Expand Down
3 changes: 3 additions & 0 deletions lib/matplotlib/widgets.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ from .figure import Figure
from .lines import Line2D
from .patches import Polygon, Rectangle
from .text import Text
from .backend_tools import Cursors

import PIL.Image

Expand Down Expand Up @@ -38,6 +39,7 @@ class AxesWidget(Widget):
def canvas(self) -> FigureCanvasBase | None: ...
def connect_event(self, event: Event, callback: Callable) -> None: ...
def disconnect_events(self) -> None: ...
def _set_cursor(self, cursor: Cursors) -> None: ...

class Button(AxesWidget):
label: Text
Expand Down Expand Up @@ -398,6 +400,7 @@ class RectangleSelector(_SelectorWidget):
minspany: float
spancoords: Literal["data", "pixels"]
grab_range: float
_active_handle: None | Literal["C", "N", "NE", "E", "SE", "S", "SW", "W", "NW"]
def __init__(
self,
ax: Axes,
Expand Down