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

Skip to content

Commit ab40ffd

Browse files
committed
Add HiDPI support in GTK
1 parent ce9a7b1 commit ab40ffd

File tree

6 files changed

+101
-45
lines changed

6 files changed

+101
-45
lines changed

lib/matplotlib/backends/backend_gtk3.py

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ def __init__(self, figure=None):
109109
self.connect('button_press_event', self.button_press_event)
110110
self.connect('button_release_event', self.button_release_event)
111111
self.connect('configure_event', self.configure_event)
112+
self.connect('screen-changed', self._update_device_pixel_ratio)
113+
self.connect('notify::scale-factor', self._update_device_pixel_ratio)
112114
self.connect('draw', self.on_draw_event)
113115
self.connect('draw', self._post_draw)
114116
self.connect('key_press_event', self.key_press_event)
@@ -139,26 +141,35 @@ def set_cursor(self, cursor):
139141
context = GLib.MainContext.default()
140142
context.iteration(True)
141143

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

150165
def button_press_event(self, widget, event):
151-
x = event.x
152-
# flipy so y=0 is bottom of canvas
153-
y = self.get_allocation().height - event.y
166+
x, y = self._mouse_event_coords(event)
154167
FigureCanvasBase.button_press_event(
155168
self, x, y, event.button, guiEvent=event)
156169
return False # finish event propagation?
157170

158171
def button_release_event(self, widget, event):
159-
x = event.x
160-
# flipy so y=0 is bottom of canvas
161-
y = self.get_allocation().height - event.y
172+
x, y = self._mouse_event_coords(event)
162173
FigureCanvasBase.button_release_event(
163174
self, x, y, event.button, guiEvent=event)
164175
return False # finish event propagation?
@@ -176,27 +187,26 @@ def key_release_event(self, widget, event):
176187
def motion_notify_event(self, widget, event):
177188
if event.is_hint:
178189
t, x, y, state = event.window.get_device_position(event.device)
190+
# flipy so y=0 is bottom of canvas
191+
x *= self.device_pixel_ratio
192+
y = (self.get_allocation().height - y) * self.device_pixel_ratio
179193
else:
180-
x, y = event.x, event.y
194+
x, y = self._mouse_event_coords(event)
181195

182-
# flipy so y=0 is bottom of canvas
183-
y = self.get_allocation().height - y
184196
FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event)
185197
return False # finish event propagation?
186198

187199
def leave_notify_event(self, widget, event):
188200
FigureCanvasBase.leave_notify_event(self, event)
189201

190202
def enter_notify_event(self, widget, event):
191-
x = event.x
192-
# flipy so y=0 is bottom of canvas
193-
y = self.get_allocation().height - event.y
203+
x, y = self._mouse_event_coords(event)
194204
FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y))
195205

196206
def size_allocate(self, widget, allocation):
197207
dpival = self.figure.dpi
198-
winch = allocation.width / dpival
199-
hinch = allocation.height / dpival
208+
winch = allocation.width * self.device_pixel_ratio / dpival
209+
hinch = allocation.height * self.device_pixel_ratio / dpival
200210
self.figure.set_size_inches(winch, hinch, forward=False)
201211
FigureCanvasBase.resize_event(self)
202212
self.draw_idle()
@@ -218,10 +228,21 @@ def _get_key(self, event):
218228
key = f'{prefix}+{key}'
219229
return key
220230

231+
def _update_device_pixel_ratio(self, *args, **kwargs):
232+
# We need to be careful in cases with mixed resolution displays if
233+
# device_pixel_ratio changes.
234+
if self._set_device_pixel_ratio(self.get_scale_factor()):
235+
# The easiest way to resize the canvas is to emit a resize event
236+
# since we implement all the logic for resizing the canvas for that
237+
# event.
238+
self.queue_resize()
239+
self.queue_draw()
240+
221241
def configure_event(self, widget, event):
222242
if widget.get_property("window") is None:
223243
return
224-
w, h = event.width, event.height
244+
w = event.width * self.device_pixel_ratio
245+
h = event.height * self.device_pixel_ratio
225246
if w < 3 or h < 3:
226247
return # empty fig
227248
# resize the figure (in inches)
@@ -238,7 +259,8 @@ def _post_draw(self, widget, ctx):
238259
if self._rubberband_rect is None:
239260
return
240261

241-
x0, y0, w, h = self._rubberband_rect
262+
x0, y0, w, h = (dim / self.device_pixel_ratio
263+
for dim in self._rubberband_rect)
242264
x1 = x0 + w
243265
y1 = y0 + h
244266

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.figure.draw(self._renderer)
3234

3335

lib/matplotlib/backends/backend_gtk4.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def __init__(self, figure=None):
6666

6767
self.set_draw_func(self._draw_func)
6868
self.connect('resize', self.resize_event)
69+
self.connect('notify::scale-factor', self._update_device_pixel_ratio)
6970

7071
click = Gtk.GestureClick()
7172
click.set_button(0) # All buttons.
@@ -109,20 +110,33 @@ def set_cursor(self, cursor):
109110
# docstring inherited
110111
self.set_cursor_from_name(_mpl_to_gtk_cursor(cursor))
111112

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+
112128
def scroll_event(self, controller, dx, dy):
113129
FigureCanvasBase.scroll_event(self, 0, 0, dy)
114130
return True
115131

116132
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
133+
x, y = self._mouse_event_coords(x, y)
119134
FigureCanvasBase.button_press_event(self, x, y,
120135
controller.get_current_button())
121136
self.grab_focus()
122137

123138
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
139+
x, y = self._mouse_event_coords(x, y)
126140
FigureCanvasBase.button_release_event(self, x, y,
127141
controller.get_current_button())
128142

@@ -137,21 +151,22 @@ def key_release_event(self, controller, keyval, keycode, state):
137151
return True
138152

139153
def motion_notify_event(self, controller, x, y):
140-
# flipy so y=0 is bottom of canvas
141-
y = self.get_allocation().height - y
154+
x, y = self._mouse_event_coords(x, y)
142155
FigureCanvasBase.motion_notify_event(self, x, y)
143156

144157
def leave_notify_event(self, controller):
145158
FigureCanvasBase.leave_notify_event(self)
146159

147160
def enter_notify_event(self, controller, x, y):
148-
# flipy so y=0 is bottom of canvas
149-
y = self.get_allocation().height - y
161+
x, y = self._mouse_event_coords(x, y)
150162
FigureCanvasBase.enter_notify_event(self, xy=(x, y))
151163

152164
def resize_event(self, area, width, height):
165+
self._update_device_pixel_ratio()
153166
dpi = self.figure.dpi
154-
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)
155170
FigureCanvasBase.resize_event(self)
156171
self.draw_idle()
157172

@@ -172,6 +187,12 @@ def _get_key(self, keyval, keycode, state):
172187
key = f'{prefix}+{key}'
173188
return key
174189

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+
175196
def _draw_rubberband(self, rect):
176197
self._rubberband_rect = rect
177198
# TODO: Only update the rubberband area.
@@ -185,7 +206,8 @@ def _post_draw(self, widget, ctx):
185206
if self._rubberband_rect is None:
186207
return
187208

188-
x0, y0, w, h = self._rubberband_rect
209+
x0, y0, w, h = (dim / self.device_pixel_ratio
210+
for dim in self._rubberband_rect)
189211
x1 = x0 + w
190212
y1 = y0 + h
191213

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)

lib/matplotlib/backends/backend_gtk4cairo.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@ def on_draw_event(self, widget, ctx):
2020
with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
2121
else nullcontext()):
2222
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)
2326
allocation = self.get_allocation()
2427
Gtk.render_background(
2528
self.get_style_context(), ctx,
2629
allocation.x, allocation.y,
2730
allocation.width, allocation.height)
2831
self._renderer.set_width_height(
29-
allocation.width, allocation.height)
32+
allocation.width * scale, allocation.height * scale)
3033
self.figure.draw(self._renderer)
3134

3235

0 commit comments

Comments
 (0)