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

Skip to content

Commit c58564e

Browse files
authored
Merge pull request #20988 from QuLogic/gtk-hidpi
Add HiDPI support in GTK.
2 parents c53216b + 76c2c7f commit c58564e

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
@@ -93,7 +93,6 @@ class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase):
9393
| Gdk.EventMask.ENTER_NOTIFY_MASK
9494
| Gdk.EventMask.LEAVE_NOTIFY_MASK
9595
| Gdk.EventMask.POINTER_MOTION_MASK
96-
| Gdk.EventMask.POINTER_MOTION_HINT_MASK
9796
| Gdk.EventMask.SCROLL_MASK)
9897

9998
def __init__(self, figure=None):
@@ -108,6 +107,8 @@ def __init__(self, figure=None):
108107
self.connect('button_press_event', self.button_press_event)
109108
self.connect('button_release_event', self.button_release_event)
110109
self.connect('configure_event', self.configure_event)
110+
self.connect('screen-changed', self._update_device_pixel_ratio)
111+
self.connect('notify::scale-factor', self._update_device_pixel_ratio)
111112
self.connect('draw', self.on_draw_event)
112113
self.connect('draw', self._post_draw)
113114
self.connect('key_press_event', self.key_press_event)
@@ -138,26 +139,35 @@ def set_cursor(self, cursor):
138139
context = GLib.MainContext.default()
139140
context.iteration(True)
140141

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

149163
def button_press_event(self, widget, event):
150-
x = event.x
151-
# flipy so y=0 is bottom of canvas
152-
y = self.get_allocation().height - event.y
164+
x, y = self._mouse_event_coords(event)
153165
FigureCanvasBase.button_press_event(
154166
self, x, y, event.button, guiEvent=event)
155167
return False # finish event propagation?
156168

157169
def button_release_event(self, widget, event):
158-
x = event.x
159-
# flipy so y=0 is bottom of canvas
160-
y = self.get_allocation().height - event.y
170+
x, y = self._mouse_event_coords(event)
161171
FigureCanvasBase.button_release_event(
162172
self, x, y, event.button, guiEvent=event)
163173
return False # finish event propagation?
@@ -173,29 +183,21 @@ def key_release_event(self, widget, event):
173183
return True # stop event propagation
174184

175185
def motion_notify_event(self, widget, event):
176-
if event.is_hint:
177-
t, x, y, state = event.window.get_device_position(event.device)
178-
else:
179-
x, y = event.x, event.y
180-
181-
# flipy so y=0 is bottom of canvas
182-
y = self.get_allocation().height - y
186+
x, y = self._mouse_event_coords(event)
183187
FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event)
184188
return False # finish event propagation?
185189

186190
def leave_notify_event(self, widget, event):
187191
FigureCanvasBase.leave_notify_event(self, event)
188192

189193
def enter_notify_event(self, widget, event):
190-
x = event.x
191-
# flipy so y=0 is bottom of canvas
192-
y = self.get_allocation().height - event.y
194+
x, y = self._mouse_event_coords(event)
193195
FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y))
194196

195197
def size_allocate(self, widget, allocation):
196198
dpival = self.figure.dpi
197-
winch = allocation.width / dpival
198-
hinch = allocation.height / dpival
199+
winch = allocation.width * self.device_pixel_ratio / dpival
200+
hinch = allocation.height * self.device_pixel_ratio / dpival
199201
self.figure.set_size_inches(winch, hinch, forward=False)
200202
FigureCanvasBase.resize_event(self)
201203
self.draw_idle()
@@ -217,10 +219,21 @@ def _get_key(self, event):
217219
key = f'{prefix}+{key}'
218220
return key
219221

222+
def _update_device_pixel_ratio(self, *args, **kwargs):
223+
# We need to be careful in cases with mixed resolution displays if
224+
# device_pixel_ratio changes.
225+
if self._set_device_pixel_ratio(self.get_scale_factor()):
226+
# The easiest way to resize the canvas is to emit a resize event
227+
# since we implement all the logic for resizing the canvas for that
228+
# event.
229+
self.queue_resize()
230+
self.queue_draw()
231+
220232
def configure_event(self, widget, event):
221233
if widget.get_property("window") is None:
222234
return
223-
w, h = event.width, event.height
235+
w = event.width * self.device_pixel_ratio
236+
h = event.height * self.device_pixel_ratio
224237
if w < 3 or h < 3:
225238
return # empty fig
226239
# resize the figure (in inches)
@@ -237,7 +250,8 @@ def _post_draw(self, widget, ctx):
237250
if self._rubberband_rect is None:
238251
return
239252

240-
x0, y0, w, h = self._rubberband_rect
253+
x0, y0, w, h = (dim / self.device_pixel_ratio
254+
for dim in self._rubberband_rect)
241255
x1 = x0 + w
242256
y1 = y0 + h
243257

@@ -326,8 +340,7 @@ def __init__(self, canvas, num):
326340

327341
self.vbox.pack_start(self.canvas, True, True, 0)
328342
# calculate size for window
329-
w = int(self.canvas.figure.bbox.width)
330-
h = int(self.canvas.figure.bbox.height)
343+
w, h = self.canvas.get_width_height()
331344

332345
self.toolbar = self._get_toolbar()
333346

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
@@ -52,6 +52,7 @@ def _mpl_to_gtk_cursor(mpl_cursor):
5252
class FigureCanvasGTK4(Gtk.DrawingArea, FigureCanvasBase):
5353
required_interactive_framework = "gtk4"
5454
_timer_cls = TimerGTK4
55+
_context_is_scaled = False
5556

5657
def __init__(self, figure=None):
5758
FigureCanvasBase.__init__(self, figure)
@@ -65,6 +66,7 @@ def __init__(self, figure=None):
6566

6667
self.set_draw_func(self._draw_func)
6768
self.connect('resize', self.resize_event)
69+
self.connect('notify::scale-factor', self._update_device_pixel_ratio)
6870

6971
click = Gtk.GestureClick()
7072
click.set_button(0) # All buttons.
@@ -108,20 +110,33 @@ def set_cursor(self, cursor):
108110
# docstring inherited
109111
self.set_cursor_from_name(_mpl_to_gtk_cursor(cursor))
110112

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

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

122138
def button_release_event(self, controller, n_press, x, y):
123-
# flipy so y=0 is bottom of canvas
124-
y = self.get_allocation().height - y
139+
x, y = self._mouse_event_coords(x, y)
125140
FigureCanvasBase.button_release_event(self, x, y,
126141
controller.get_current_button())
127142

@@ -136,21 +151,22 @@ def key_release_event(self, controller, keyval, keycode, state):
136151
return True
137152

138153
def motion_notify_event(self, controller, x, y):
139-
# flipy so y=0 is bottom of canvas
140-
y = self.get_allocation().height - y
154+
x, y = self._mouse_event_coords(x, y)
141155
FigureCanvasBase.motion_notify_event(self, x, y)
142156

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

146160
def enter_notify_event(self, controller, x, y):
147-
# flipy so y=0 is bottom of canvas
148-
y = self.get_allocation().height - y
161+
x, y = self._mouse_event_coords(x, y)
149162
FigureCanvasBase.enter_notify_event(self, xy=(x, y))
150163

151164
def resize_event(self, area, width, height):
165+
self._update_device_pixel_ratio()
152166
dpi = self.figure.dpi
153-
self.figure.set_size_inches(width / dpi, height / dpi, forward=False)
167+
winch = width * self.device_pixel_ratio / dpi
168+
hinch = height * self.device_pixel_ratio / dpi
169+
self.figure.set_size_inches(winch, hinch, forward=False)
154170
FigureCanvasBase.resize_event(self)
155171
self.draw_idle()
156172

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

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

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

@@ -200,12 +230,12 @@ def _post_draw(self, widget, ctx):
200230
ctx.line_to(x1, y1)
201231

202232
ctx.set_antialias(1)
203-
ctx.set_line_width(1)
204-
ctx.set_dash((3, 3), 0)
233+
ctx.set_line_width(lw)
234+
ctx.set_dash((dash, dash), 0)
205235
ctx.set_source_rgb(0, 0, 0)
206236
ctx.stroke_preserve()
207237

208-
ctx.set_dash((3, 3), 3)
238+
ctx.set_dash((dash, dash), dash)
209239
ctx.set_source_rgb(1, 1, 1)
210240
ctx.stroke()
211241

@@ -265,8 +295,7 @@ def __init__(self, canvas, num):
265295

266296
self.vbox.prepend(self.canvas)
267297
# calculate size for window
268-
w = int(self.canvas.figure.bbox.width)
269-
h = int(self.canvas.figure.bbox.height)
298+
w, h = self.canvas.get_width_height()
270299

271300
self.toolbar = self._get_toolbar()
272301

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)