Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c08c697

Browse files
authored
Merge pull request #21084 from meeseeksmachine/auto-backport-of-pr-20988-on-v3.5.x
Backport PR #20988 on branch v3.5.x (Add HiDPI support in GTK.)
2 parents 69ddca2 + 6f39568 commit c08c697

File tree

6 files changed

+112
-57
lines changed

6 files changed

+112
-57
lines changed

lib/matplotlib/backends/backend_gtk3.py

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase):
8787
| Gdk.EventMask.ENTER_NOTIFY_MASK
8888
| Gdk.EventMask.LEAVE_NOTIFY_MASK
8989
| Gdk.EventMask.POINTER_MOTION_MASK
90-
| Gdk.EventMask.POINTER_MOTION_HINT_MASK
9190
| Gdk.EventMask.SCROLL_MASK)
9291

9392
def __init__(self, figure=None):
@@ -102,6 +101,8 @@ def __init__(self, figure=None):
102101
self.connect('button_press_event', self.button_press_event)
103102
self.connect('button_release_event', self.button_release_event)
104103
self.connect('configure_event', self.configure_event)
104+
self.connect('screen-changed', self._update_device_pixel_ratio)
105+
self.connect('notify::scale-factor', self._update_device_pixel_ratio)
105106
self.connect('draw', self.on_draw_event)
106107
self.connect('draw', self._post_draw)
107108
self.connect('key_press_event', self.key_press_event)
@@ -132,26 +133,35 @@ def set_cursor(self, cursor):
132133
context = GLib.MainContext.default()
133134
context.iteration(True)
134135

136+
def _mouse_event_coords(self, event):
137+
"""
138+
Calculate mouse coordinates in physical pixels.
139+
140+
GTK use logical pixels, but the figure is scaled to physical pixels for
141+
rendering. Transform to physical pixels so that all of the down-stream
142+
transforms work as expected.
143+
144+
Also, the origin is different and needs to be corrected.
145+
"""
146+
x = event.x * self.device_pixel_ratio
147+
# flip y so y=0 is bottom of canvas
148+
y = self.figure.bbox.height - event.y * self.device_pixel_ratio
149+
return x, y
150+
135151
def scroll_event(self, widget, event):
136-
x = event.x
137-
# flipy so y=0 is bottom of canvas
138-
y = self.get_allocation().height - event.y
152+
x, y = self._mouse_event_coords(event)
139153
step = 1 if event.direction == Gdk.ScrollDirection.UP else -1
140154
FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event)
141155
return False # finish event propagation?
142156

143157
def button_press_event(self, widget, event):
144-
x = event.x
145-
# flipy so y=0 is bottom of canvas
146-
y = self.get_allocation().height - event.y
158+
x, y = self._mouse_event_coords(event)
147159
FigureCanvasBase.button_press_event(
148160
self, x, y, event.button, guiEvent=event)
149161
return False # finish event propagation?
150162

151163
def button_release_event(self, widget, event):
152-
x = event.x
153-
# flipy so y=0 is bottom of canvas
154-
y = self.get_allocation().height - event.y
164+
x, y = self._mouse_event_coords(event)
155165
FigureCanvasBase.button_release_event(
156166
self, x, y, event.button, guiEvent=event)
157167
return False # finish event propagation?
@@ -167,29 +177,21 @@ def key_release_event(self, widget, event):
167177
return True # stop event propagation
168178

169179
def motion_notify_event(self, widget, event):
170-
if event.is_hint:
171-
t, x, y, state = event.window.get_device_position(event.device)
172-
else:
173-
x, y = event.x, event.y
174-
175-
# flipy so y=0 is bottom of canvas
176-
y = self.get_allocation().height - y
180+
x, y = self._mouse_event_coords(event)
177181
FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event)
178182
return False # finish event propagation?
179183

180184
def leave_notify_event(self, widget, event):
181185
FigureCanvasBase.leave_notify_event(self, event)
182186

183187
def enter_notify_event(self, widget, event):
184-
x = event.x
185-
# flipy so y=0 is bottom of canvas
186-
y = self.get_allocation().height - event.y
188+
x, y = self._mouse_event_coords(event)
187189
FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y))
188190

189191
def size_allocate(self, widget, allocation):
190192
dpival = self.figure.dpi
191-
winch = allocation.width / dpival
192-
hinch = allocation.height / dpival
193+
winch = allocation.width * self.device_pixel_ratio / dpival
194+
hinch = allocation.height * self.device_pixel_ratio / dpival
193195
self.figure.set_size_inches(winch, hinch, forward=False)
194196
FigureCanvasBase.resize_event(self)
195197
self.draw_idle()
@@ -211,10 +213,21 @@ def _get_key(self, event):
211213
key = f'{prefix}+{key}'
212214
return key
213215

216+
def _update_device_pixel_ratio(self, *args, **kwargs):
217+
# We need to be careful in cases with mixed resolution displays if
218+
# device_pixel_ratio changes.
219+
if self._set_device_pixel_ratio(self.get_scale_factor()):
220+
# The easiest way to resize the canvas is to emit a resize event
221+
# since we implement all the logic for resizing the canvas for that
222+
# event.
223+
self.queue_resize()
224+
self.queue_draw()
225+
214226
def configure_event(self, widget, event):
215227
if widget.get_property("window") is None:
216228
return
217-
w, h = event.width, event.height
229+
w = event.width * self.device_pixel_ratio
230+
h = event.height * self.device_pixel_ratio
218231
if w < 3 or h < 3:
219232
return # empty fig
220233
# resize the figure (in inches)
@@ -231,7 +244,8 @@ def _post_draw(self, widget, ctx):
231244
if self._rubberband_rect is None:
232245
return
233246

234-
x0, y0, w, h = self._rubberband_rect
247+
x0, y0, w, h = (dim / self.device_pixel_ratio
248+
for dim in self._rubberband_rect)
235249
x1 = x0 + w
236250
y1 = y0 + h
237251

@@ -318,8 +332,7 @@ def __init__(self, canvas, num):
318332

319333
self.vbox.pack_start(self.canvas, True, True, 0)
320334
# calculate size for window
321-
w = int(self.canvas.figure.bbox.width)
322-
h = int(self.canvas.figure.bbox.height)
335+
w, h = self.canvas.get_width_height()
323336

324337
self.toolbar = self._get_toolbar()
325338

lib/matplotlib/backends/backend_gtk3agg.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ def __init__(self, figure):
1818
self._bbox_queue = []
1919

2020
def on_draw_event(self, widget, ctx):
21-
"""GtkDrawable draw event, like expose_event in GTK 2.X."""
21+
scale = self.device_pixel_ratio
2222
allocation = self.get_allocation()
23-
w, h = allocation.width, allocation.height
23+
w = allocation.width * scale
24+
h = allocation.height * scale
2425

2526
if not len(self._bbox_queue):
2627
Gtk.render_background(
@@ -43,7 +44,8 @@ def on_draw_event(self, widget, ctx):
4344
np.asarray(self.copy_from_bbox(bbox)))
4445
image = cairo.ImageSurface.create_for_data(
4546
buf.ravel().data, cairo.FORMAT_ARGB32, width, height)
46-
ctx.set_source_surface(image, x, y)
47+
image.set_device_scale(scale, scale)
48+
ctx.set_source_surface(image, x / scale, y / scale)
4749
ctx.paint()
4850

4951
if len(self._bbox_queue):
@@ -57,11 +59,12 @@ def blit(self, bbox=None):
5759
if bbox is None:
5860
bbox = self.figure.bbox
5961

62+
scale = self.device_pixel_ratio
6063
allocation = self.get_allocation()
61-
x = int(bbox.x0)
62-
y = allocation.height - int(bbox.y1)
63-
width = int(bbox.x1) - int(bbox.x0)
64-
height = int(bbox.y1) - int(bbox.y0)
64+
x = int(bbox.x0 / scale)
65+
y = allocation.height - int(bbox.y1 / scale)
66+
width = (int(bbox.x1) - int(bbox.x0)) // scale
67+
height = (int(bbox.y1) - int(bbox.y0)) // scale
6568

6669
self._bbox_queue.append(bbox)
6770
self.queue_draw_area(x, y, width, height)

lib/matplotlib/backends/backend_gtk3cairo.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@ def __init__(self, figure):
1717
self._renderer = RendererGTK3Cairo(self.figure.dpi)
1818

1919
def on_draw_event(self, widget, ctx):
20-
"""GtkDrawable draw event."""
2120
with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
2221
else nullcontext()):
2322
self._renderer.set_context(ctx)
23+
scale = self.device_pixel_ratio
24+
# Scale physical drawing to logical size.
25+
ctx.scale(1 / scale, 1 / scale)
2426
allocation = self.get_allocation()
2527
Gtk.render_background(
2628
self.get_style_context(), ctx,
2729
allocation.x, allocation.y,
2830
allocation.width, allocation.height)
2931
self._renderer.set_width_height(
30-
allocation.width, allocation.height)
32+
allocation.width * scale, allocation.height * scale)
3133
self._renderer.dpi = self.figure.dpi
3234
self.figure.draw(self._renderer)
3335

lib/matplotlib/backends/backend_gtk4.py

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def _mpl_to_gtk_cursor(mpl_cursor):
5353
class FigureCanvasGTK4(Gtk.DrawingArea, FigureCanvasBase):
5454
required_interactive_framework = "gtk4"
5555
_timer_cls = TimerGTK4
56+
_context_is_scaled = False
5657

5758
def __init__(self, figure=None):
5859
FigureCanvasBase.__init__(self, figure)
@@ -66,6 +67,7 @@ def __init__(self, figure=None):
6667

6768
self.set_draw_func(self._draw_func)
6869
self.connect('resize', self.resize_event)
70+
self.connect('notify::scale-factor', self._update_device_pixel_ratio)
6971

7072
click = Gtk.GestureClick()
7173
click.set_button(0) # All buttons.
@@ -109,20 +111,33 @@ def set_cursor(self, cursor):
109111
# docstring inherited
110112
self.set_cursor_from_name(_mpl_to_gtk_cursor(cursor))
111113

114+
def _mouse_event_coords(self, x, y):
115+
"""
116+
Calculate mouse coordinates in physical pixels.
117+
118+
GTK use logical pixels, but the figure is scaled to physical pixels for
119+
rendering. Transform to physical pixels so that all of the down-stream
120+
transforms work as expected.
121+
122+
Also, the origin is different and needs to be corrected.
123+
"""
124+
x = x * self.device_pixel_ratio
125+
# flip y so y=0 is bottom of canvas
126+
y = self.figure.bbox.height - y * self.device_pixel_ratio
127+
return x, y
128+
112129
def scroll_event(self, controller, dx, dy):
113130
FigureCanvasBase.scroll_event(self, 0, 0, dy)
114131
return True
115132

116133
def button_press_event(self, controller, n_press, x, y):
117-
# flipy so y=0 is bottom of canvas
118-
y = self.get_allocation().height - y
134+
x, y = self._mouse_event_coords(x, y)
119135
FigureCanvasBase.button_press_event(self, x, y,
120136
controller.get_current_button())
121137
self.grab_focus()
122138

123139
def button_release_event(self, controller, n_press, x, y):
124-
# flipy so y=0 is bottom of canvas
125-
y = self.get_allocation().height - y
140+
x, y = self._mouse_event_coords(x, y)
126141
FigureCanvasBase.button_release_event(self, x, y,
127142
controller.get_current_button())
128143

@@ -137,21 +152,22 @@ def key_release_event(self, controller, keyval, keycode, state):
137152
return True
138153

139154
def motion_notify_event(self, controller, x, y):
140-
# flipy so y=0 is bottom of canvas
141-
y = self.get_allocation().height - y
155+
x, y = self._mouse_event_coords(x, y)
142156
FigureCanvasBase.motion_notify_event(self, x, y)
143157

144158
def leave_notify_event(self, controller):
145159
FigureCanvasBase.leave_notify_event(self)
146160

147161
def enter_notify_event(self, controller, x, y):
148-
# flipy so y=0 is bottom of canvas
149-
y = self.get_allocation().height - y
162+
x, y = self._mouse_event_coords(x, y)
150163
FigureCanvasBase.enter_notify_event(self, xy=(x, y))
151164

152165
def resize_event(self, area, width, height):
166+
self._update_device_pixel_ratio()
153167
dpi = self.figure.dpi
154-
self.figure.set_size_inches(width / dpi, height / dpi, forward=False)
168+
winch = width * self.device_pixel_ratio / dpi
169+
hinch = height * self.device_pixel_ratio / dpi
170+
self.figure.set_size_inches(winch, hinch, forward=False)
155171
FigureCanvasBase.resize_event(self)
156172
self.draw_idle()
157173

@@ -172,6 +188,12 @@ def _get_key(self, keyval, keycode, state):
172188
key = f'{prefix}+{key}'
173189
return key
174190

191+
def _update_device_pixel_ratio(self, *args, **kwargs):
192+
# We need to be careful in cases with mixed resolution displays if
193+
# device_pixel_ratio changes.
194+
if self._set_device_pixel_ratio(self.get_scale_factor()):
195+
self.draw()
196+
175197
def _draw_rubberband(self, rect):
176198
self._rubberband_rect = rect
177199
# TODO: Only update the rubberband area.
@@ -185,7 +207,15 @@ def _post_draw(self, widget, ctx):
185207
if self._rubberband_rect is None:
186208
return
187209

188-
x0, y0, w, h = self._rubberband_rect
210+
lw = 1
211+
dash = 3
212+
if not self._context_is_scaled:
213+
x0, y0, w, h = (dim / self.device_pixel_ratio
214+
for dim in self._rubberband_rect)
215+
else:
216+
x0, y0, w, h = self._rubberband_rect
217+
lw *= self.device_pixel_ratio
218+
dash *= self.device_pixel_ratio
189219
x1 = x0 + w
190220
y1 = y0 + h
191221

@@ -201,12 +231,12 @@ def _post_draw(self, widget, ctx):
201231
ctx.line_to(x1, y1)
202232

203233
ctx.set_antialias(1)
204-
ctx.set_line_width(1)
205-
ctx.set_dash((3, 3), 0)
234+
ctx.set_line_width(lw)
235+
ctx.set_dash((dash, dash), 0)
206236
ctx.set_source_rgb(0, 0, 0)
207237
ctx.stroke_preserve()
208238

209-
ctx.set_dash((3, 3), 3)
239+
ctx.set_dash((dash, dash), dash)
210240
ctx.set_source_rgb(1, 1, 1)
211241
ctx.stroke()
212242

@@ -266,8 +296,7 @@ def __init__(self, canvas, num):
266296

267297
self.vbox.prepend(self.canvas)
268298
# calculate size for window
269-
w = int(self.canvas.figure.bbox.width)
270-
h = int(self.canvas.figure.bbox.height)
299+
w, h = self.canvas.get_width_height()
271300

272301
self.toolbar = self._get_toolbar()
273302

lib/matplotlib/backends/backend_gtk4agg.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ def __init__(self, figure):
1818
self._bbox_queue = []
1919

2020
def on_draw_event(self, widget, ctx):
21+
scale = self.device_pixel_ratio
2122
allocation = self.get_allocation()
22-
w, h = allocation.width, allocation.height
23+
w = allocation.width * scale
24+
h = allocation.height * scale
2325

2426
if not len(self._bbox_queue):
2527
Gtk.render_background(
@@ -42,7 +44,8 @@ def on_draw_event(self, widget, ctx):
4244
np.asarray(self.copy_from_bbox(bbox)))
4345
image = cairo.ImageSurface.create_for_data(
4446
buf.ravel().data, cairo.FORMAT_ARGB32, width, height)
45-
ctx.set_source_surface(image, x, y)
47+
image.set_device_scale(scale, scale)
48+
ctx.set_source_surface(image, x / scale, y / scale)
4649
ctx.paint()
4750

4851
if len(self._bbox_queue):
@@ -56,11 +59,12 @@ def blit(self, bbox=None):
5659
if bbox is None:
5760
bbox = self.figure.bbox
5861

62+
scale = self.device_pixel_ratio
5963
allocation = self.get_allocation()
60-
x = int(bbox.x0)
61-
y = allocation.height - int(bbox.y1)
62-
width = int(bbox.x1) - int(bbox.x0)
63-
height = int(bbox.y1) - int(bbox.y0)
64+
x = int(bbox.x0 / scale)
65+
y = allocation.height - int(bbox.y1 / scale)
66+
width = (int(bbox.x1) - int(bbox.x0)) // scale
67+
height = (int(bbox.y1) - int(bbox.y0)) // scale
6468

6569
self._bbox_queue.append(bbox)
6670
self.queue_draw_area(x, y, width, height)

0 commit comments

Comments
 (0)