diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 7701d7742bf0..662d1028658e 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -87,7 +87,6 @@ class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase): | Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK | Gdk.EventMask.POINTER_MOTION_MASK - | Gdk.EventMask.POINTER_MOTION_HINT_MASK | Gdk.EventMask.SCROLL_MASK) def __init__(self, figure=None): @@ -102,6 +101,8 @@ def __init__(self, figure=None): self.connect('button_press_event', self.button_press_event) self.connect('button_release_event', self.button_release_event) self.connect('configure_event', self.configure_event) + self.connect('screen-changed', self._update_device_pixel_ratio) + self.connect('notify::scale-factor', self._update_device_pixel_ratio) self.connect('draw', self.on_draw_event) self.connect('draw', self._post_draw) self.connect('key_press_event', self.key_press_event) @@ -132,26 +133,35 @@ def set_cursor(self, cursor): context = GLib.MainContext.default() context.iteration(True) + def _mouse_event_coords(self, event): + """ + Calculate mouse coordinates in physical pixels. + + GTK use logical pixels, but the figure is scaled to physical pixels for + rendering. Transform to physical pixels so that all of the down-stream + transforms work as expected. + + Also, the origin is different and needs to be corrected. + """ + x = event.x * self.device_pixel_ratio + # flip y so y=0 is bottom of canvas + y = self.figure.bbox.height - event.y * self.device_pixel_ratio + return x, y + def scroll_event(self, widget, event): - x = event.x - # flipy so y=0 is bottom of canvas - y = self.get_allocation().height - event.y + x, y = self._mouse_event_coords(event) step = 1 if event.direction == Gdk.ScrollDirection.UP else -1 FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event) return False # finish event propagation? def button_press_event(self, widget, event): - x = event.x - # flipy so y=0 is bottom of canvas - y = self.get_allocation().height - event.y + x, y = self._mouse_event_coords(event) FigureCanvasBase.button_press_event( self, x, y, event.button, guiEvent=event) return False # finish event propagation? def button_release_event(self, widget, event): - x = event.x - # flipy so y=0 is bottom of canvas - y = self.get_allocation().height - event.y + x, y = self._mouse_event_coords(event) FigureCanvasBase.button_release_event( self, x, y, event.button, guiEvent=event) return False # finish event propagation? @@ -167,13 +177,7 @@ def key_release_event(self, widget, event): return True # stop event propagation def motion_notify_event(self, widget, event): - if event.is_hint: - t, x, y, state = event.window.get_device_position(event.device) - else: - x, y = event.x, event.y - - # flipy so y=0 is bottom of canvas - y = self.get_allocation().height - y + x, y = self._mouse_event_coords(event) FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event) return False # finish event propagation? @@ -181,15 +185,13 @@ def leave_notify_event(self, widget, event): FigureCanvasBase.leave_notify_event(self, event) def enter_notify_event(self, widget, event): - x = event.x - # flipy so y=0 is bottom of canvas - y = self.get_allocation().height - event.y + x, y = self._mouse_event_coords(event) FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y)) def size_allocate(self, widget, allocation): dpival = self.figure.dpi - winch = allocation.width / dpival - hinch = allocation.height / dpival + winch = allocation.width * self.device_pixel_ratio / dpival + hinch = allocation.height * self.device_pixel_ratio / dpival self.figure.set_size_inches(winch, hinch, forward=False) FigureCanvasBase.resize_event(self) self.draw_idle() @@ -211,10 +213,21 @@ def _get_key(self, event): key = f'{prefix}+{key}' return key + def _update_device_pixel_ratio(self, *args, **kwargs): + # We need to be careful in cases with mixed resolution displays if + # device_pixel_ratio changes. + if self._set_device_pixel_ratio(self.get_scale_factor()): + # The easiest way to resize the canvas is to emit a resize event + # since we implement all the logic for resizing the canvas for that + # event. + self.queue_resize() + self.queue_draw() + def configure_event(self, widget, event): if widget.get_property("window") is None: return - w, h = event.width, event.height + w = event.width * self.device_pixel_ratio + h = event.height * self.device_pixel_ratio if w < 3 or h < 3: return # empty fig # resize the figure (in inches) @@ -231,7 +244,8 @@ def _post_draw(self, widget, ctx): if self._rubberband_rect is None: return - x0, y0, w, h = self._rubberband_rect + x0, y0, w, h = (dim / self.device_pixel_ratio + for dim in self._rubberband_rect) x1 = x0 + w y1 = y0 + h @@ -318,8 +332,7 @@ def __init__(self, canvas, num): self.vbox.pack_start(self.canvas, True, True, 0) # calculate size for window - w = int(self.canvas.figure.bbox.width) - h = int(self.canvas.figure.bbox.height) + w, h = self.canvas.get_width_height() self.toolbar = self._get_toolbar() diff --git a/lib/matplotlib/backends/backend_gtk3agg.py b/lib/matplotlib/backends/backend_gtk3agg.py index 9c26e21753ae..14206484e73d 100644 --- a/lib/matplotlib/backends/backend_gtk3agg.py +++ b/lib/matplotlib/backends/backend_gtk3agg.py @@ -18,9 +18,10 @@ def __init__(self, figure): self._bbox_queue = [] def on_draw_event(self, widget, ctx): - """GtkDrawable draw event, like expose_event in GTK 2.X.""" + scale = self.device_pixel_ratio allocation = self.get_allocation() - w, h = allocation.width, allocation.height + w = allocation.width * scale + h = allocation.height * scale if not len(self._bbox_queue): Gtk.render_background( @@ -43,7 +44,8 @@ def on_draw_event(self, widget, ctx): np.asarray(self.copy_from_bbox(bbox))) image = cairo.ImageSurface.create_for_data( buf.ravel().data, cairo.FORMAT_ARGB32, width, height) - ctx.set_source_surface(image, x, y) + image.set_device_scale(scale, scale) + ctx.set_source_surface(image, x / scale, y / scale) ctx.paint() if len(self._bbox_queue): @@ -57,11 +59,12 @@ def blit(self, bbox=None): if bbox is None: bbox = self.figure.bbox + scale = self.device_pixel_ratio allocation = self.get_allocation() - x = int(bbox.x0) - y = allocation.height - int(bbox.y1) - width = int(bbox.x1) - int(bbox.x0) - height = int(bbox.y1) - int(bbox.y0) + x = int(bbox.x0 / scale) + y = allocation.height - int(bbox.y1 / scale) + width = (int(bbox.x1) - int(bbox.x0)) // scale + height = (int(bbox.y1) - int(bbox.y0)) // scale self._bbox_queue.append(bbox) self.queue_draw_area(x, y, width, height) diff --git a/lib/matplotlib/backends/backend_gtk3cairo.py b/lib/matplotlib/backends/backend_gtk3cairo.py index 9759e15adac8..2e8558a66b2c 100644 --- a/lib/matplotlib/backends/backend_gtk3cairo.py +++ b/lib/matplotlib/backends/backend_gtk3cairo.py @@ -17,17 +17,19 @@ def __init__(self, figure): self._renderer = RendererGTK3Cairo(self.figure.dpi) def on_draw_event(self, widget, ctx): - """GtkDrawable draw event.""" with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar else nullcontext()): self._renderer.set_context(ctx) + scale = self.device_pixel_ratio + # Scale physical drawing to logical size. + ctx.scale(1 / scale, 1 / scale) allocation = self.get_allocation() Gtk.render_background( self.get_style_context(), ctx, allocation.x, allocation.y, allocation.width, allocation.height) self._renderer.set_width_height( - allocation.width, allocation.height) + allocation.width * scale, allocation.height * scale) self._renderer.dpi = self.figure.dpi self.figure.draw(self._renderer) diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index 3ffff77792db..3eb7735c1edb 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -53,6 +53,7 @@ def _mpl_to_gtk_cursor(mpl_cursor): class FigureCanvasGTK4(Gtk.DrawingArea, FigureCanvasBase): required_interactive_framework = "gtk4" _timer_cls = TimerGTK4 + _context_is_scaled = False def __init__(self, figure=None): FigureCanvasBase.__init__(self, figure) @@ -66,6 +67,7 @@ def __init__(self, figure=None): self.set_draw_func(self._draw_func) self.connect('resize', self.resize_event) + self.connect('notify::scale-factor', self._update_device_pixel_ratio) click = Gtk.GestureClick() click.set_button(0) # All buttons. @@ -109,20 +111,33 @@ def set_cursor(self, cursor): # docstring inherited self.set_cursor_from_name(_mpl_to_gtk_cursor(cursor)) + def _mouse_event_coords(self, x, y): + """ + Calculate mouse coordinates in physical pixels. + + GTK use logical pixels, but the figure is scaled to physical pixels for + rendering. Transform to physical pixels so that all of the down-stream + transforms work as expected. + + Also, the origin is different and needs to be corrected. + """ + x = x * self.device_pixel_ratio + # flip y so y=0 is bottom of canvas + y = self.figure.bbox.height - y * self.device_pixel_ratio + return x, y + def scroll_event(self, controller, dx, dy): FigureCanvasBase.scroll_event(self, 0, 0, dy) return True def button_press_event(self, controller, n_press, x, y): - # flipy so y=0 is bottom of canvas - y = self.get_allocation().height - y + x, y = self._mouse_event_coords(x, y) FigureCanvasBase.button_press_event(self, x, y, controller.get_current_button()) self.grab_focus() def button_release_event(self, controller, n_press, x, y): - # flipy so y=0 is bottom of canvas - y = self.get_allocation().height - y + x, y = self._mouse_event_coords(x, y) FigureCanvasBase.button_release_event(self, x, y, controller.get_current_button()) @@ -137,21 +152,22 @@ def key_release_event(self, controller, keyval, keycode, state): return True def motion_notify_event(self, controller, x, y): - # flipy so y=0 is bottom of canvas - y = self.get_allocation().height - y + x, y = self._mouse_event_coords(x, y) FigureCanvasBase.motion_notify_event(self, x, y) def leave_notify_event(self, controller): FigureCanvasBase.leave_notify_event(self) def enter_notify_event(self, controller, x, y): - # flipy so y=0 is bottom of canvas - y = self.get_allocation().height - y + x, y = self._mouse_event_coords(x, y) FigureCanvasBase.enter_notify_event(self, xy=(x, y)) def resize_event(self, area, width, height): + self._update_device_pixel_ratio() dpi = self.figure.dpi - self.figure.set_size_inches(width / dpi, height / dpi, forward=False) + winch = width * self.device_pixel_ratio / dpi + hinch = height * self.device_pixel_ratio / dpi + self.figure.set_size_inches(winch, hinch, forward=False) FigureCanvasBase.resize_event(self) self.draw_idle() @@ -172,6 +188,12 @@ def _get_key(self, keyval, keycode, state): key = f'{prefix}+{key}' return key + def _update_device_pixel_ratio(self, *args, **kwargs): + # We need to be careful in cases with mixed resolution displays if + # device_pixel_ratio changes. + if self._set_device_pixel_ratio(self.get_scale_factor()): + self.draw() + def _draw_rubberband(self, rect): self._rubberband_rect = rect # TODO: Only update the rubberband area. @@ -185,7 +207,15 @@ def _post_draw(self, widget, ctx): if self._rubberband_rect is None: return - x0, y0, w, h = self._rubberband_rect + lw = 1 + dash = 3 + if not self._context_is_scaled: + x0, y0, w, h = (dim / self.device_pixel_ratio + for dim in self._rubberband_rect) + else: + x0, y0, w, h = self._rubberband_rect + lw *= self.device_pixel_ratio + dash *= self.device_pixel_ratio x1 = x0 + w y1 = y0 + h @@ -201,12 +231,12 @@ def _post_draw(self, widget, ctx): ctx.line_to(x1, y1) ctx.set_antialias(1) - ctx.set_line_width(1) - ctx.set_dash((3, 3), 0) + ctx.set_line_width(lw) + ctx.set_dash((dash, dash), 0) ctx.set_source_rgb(0, 0, 0) ctx.stroke_preserve() - ctx.set_dash((3, 3), 3) + ctx.set_dash((dash, dash), dash) ctx.set_source_rgb(1, 1, 1) ctx.stroke() @@ -266,8 +296,7 @@ def __init__(self, canvas, num): self.vbox.prepend(self.canvas) # calculate size for window - w = int(self.canvas.figure.bbox.width) - h = int(self.canvas.figure.bbox.height) + w, h = self.canvas.get_width_height() self.toolbar = self._get_toolbar() diff --git a/lib/matplotlib/backends/backend_gtk4agg.py b/lib/matplotlib/backends/backend_gtk4agg.py index b3439dc109cd..d47dd07fee3b 100644 --- a/lib/matplotlib/backends/backend_gtk4agg.py +++ b/lib/matplotlib/backends/backend_gtk4agg.py @@ -18,8 +18,10 @@ def __init__(self, figure): self._bbox_queue = [] def on_draw_event(self, widget, ctx): + scale = self.device_pixel_ratio allocation = self.get_allocation() - w, h = allocation.width, allocation.height + w = allocation.width * scale + h = allocation.height * scale if not len(self._bbox_queue): Gtk.render_background( @@ -42,7 +44,8 @@ def on_draw_event(self, widget, ctx): np.asarray(self.copy_from_bbox(bbox))) image = cairo.ImageSurface.create_for_data( buf.ravel().data, cairo.FORMAT_ARGB32, width, height) - ctx.set_source_surface(image, x, y) + image.set_device_scale(scale, scale) + ctx.set_source_surface(image, x / scale, y / scale) ctx.paint() if len(self._bbox_queue): @@ -56,11 +59,12 @@ def blit(self, bbox=None): if bbox is None: bbox = self.figure.bbox + scale = self.device_pixel_ratio allocation = self.get_allocation() - x = int(bbox.x0) - y = allocation.height - int(bbox.y1) - width = int(bbox.x1) - int(bbox.x0) - height = int(bbox.y1) - int(bbox.y0) + x = int(bbox.x0 / scale) + y = allocation.height - int(bbox.y1 / scale) + width = (int(bbox.x1) - int(bbox.x0)) // scale + height = (int(bbox.y1) - int(bbox.y0)) // scale self._bbox_queue.append(bbox) self.queue_draw_area(x, y, width, height) diff --git a/lib/matplotlib/backends/backend_gtk4cairo.py b/lib/matplotlib/backends/backend_gtk4cairo.py index 53d319392727..05ddef53bd92 100644 --- a/lib/matplotlib/backends/backend_gtk4cairo.py +++ b/lib/matplotlib/backends/backend_gtk4cairo.py @@ -11,6 +11,7 @@ def set_context(self, ctx): class FigureCanvasGTK4Cairo(backend_gtk4.FigureCanvasGTK4, backend_cairo.FigureCanvasCairo): + _context_is_scaled = True def __init__(self, figure): super().__init__(figure) @@ -20,13 +21,16 @@ def on_draw_event(self, widget, ctx): with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar else nullcontext()): self._renderer.set_context(ctx) + scale = self.device_pixel_ratio + # Scale physical drawing to logical size. + ctx.scale(1 / scale, 1 / scale) allocation = self.get_allocation() Gtk.render_background( self.get_style_context(), ctx, allocation.x, allocation.y, allocation.width, allocation.height) self._renderer.set_width_height( - allocation.width, allocation.height) + allocation.width * scale, allocation.height * scale) self._renderer.dpi = self.figure.dpi self.figure.draw(self._renderer)