8000 Add HiDPI support in GTK · matplotlib/matplotlib@75ecd40 · GitHub
[go: up one dir, main page]

Skip to content

Commit 75ecd40

Browse files
committed
Add HiDPI support in GTK
1 parent 2724116 commit 75ecd40

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
@@ -108,6 +108,8 @@ def __init__(self, figure=None):
108108
self.connect('button_press_event', self.button_press_event)
109109
self.connect('button_release_event', self.button_release_event)
110110
self.connect('configure_event', self.configure_event)
111+
self.connect('screen-changed', self._update_device_pixel_ratio)
112+
self.connect('notify::scale-factor', self._update_device_pixel_ratio)
111113
self.connect('draw', self.on_draw_event)
112114
self.connect('draw', self._post_draw)
113115
self.connect('key_press_event', self.key_press_event)
@@ -138,26 +140,35 @@ def set_cursor(self, cursor):
138140
context = GLib.MainContext.default()
139141
context.iteration(True)
140142

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

149164
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
165+
x, y = self._mouse_event_coords(event)
153166
FigureCanvasBase.button_press_event(
154167
self, x, y, event.button, guiEvent=event)
155168
return False # finish event propagation?
156169

157170
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
171+
x, y = self._mouse_event_coords(event)
161172
FigureCanvasBase.button_release_event(
162173
self, x, y, event.button, guiEvent=event)
163174
return False # finish event propagation?
@@ -175,27 +186,26 @@ def key_release_event(self, widget, event):
175186
def motion_notify_event(self, widget, event):
176187
if event.is_hint:
177188
t, x, y, state = event.window.get_device_position(event.device)
189+
# flipy so y=0 is bottom of canvas
190+
x *= self.device_pixel_ratio
191+
y = (self.get_allocation().height - y) * self.device_pixel_ratio
178192
else:
179-
x, y = event.x, event.y
193+
x, y = self._mouse_event_coords(event)
180194

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

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

189201
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
202+
x, y = self._mouse_event_coords(event)
193203
FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y))
194204

195205
def size_allocate(self, widget, allocation):
196206
dpival = self.figure.dpi
197-
winch = allocation.width / dpival
198-
hinch = allocation.height / dpival
207+
winch = allocation.width * self.device_pixel_ratio / dpival
208+
hinch = allocation.height * self.device_pixel_ratio / dpival
199209
self.figure.set_size_inches(winch, hinch, forward=False)
200210
FigureCanvasBase.resize_event(self)
201211
self.draw_idle()
@@ -217,10 +227,21 @@ def _get_key(self, event):
217227
key = f'{prefix}+{key}'
218228
return key
219229

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

240-
x0, y0, w, h = self._rubberband_rect
261+
x0, y0, w, h = (dim / self.device_pixel_ratio
262+
for dim in self._rubberband_rect)
241263
x1 = x0 + w
242264
y1 = y0 + h
243265

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: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def __init__(self, figure=None):
6565

6666
self.set_draw_func(self._draw_func)
6767
self.connect('resize', self.resize_event)
68+
self.connect('notify::scale-factor', self._update_device_pixel_ratio)
6869

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

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

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

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

@@ -136,21 +150,22 @@ def key_release_event(self, controller, keyval, keycode, state):
136150
return True
137151

138152
def motion_notify_event(self, controller, x, y):
139-
# flipy so y=0 is bottom of canvas
140-
y = self.get_allocation().height - y
153+
x, y = self._mouse_event_coords(x, y)
141154
FigureCanvasBase.motion_notify_event(self, x, y)
142155

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

146159
def enter_notify_event(self, controller, x, y):
147-
# flipy so y=0 is bottom of canvas
148-
y = self.get_allocation().height - y
160+
x, y = self._mouse_event_coords(x, y)
149161
FigureCanvasBase.enter_notify_event(self, xy=(x, y))
150162

151163
def resize_event(self, area, width, height):
164+
self._update_device_pixel_ratio()
152165
dpi = self.figure.dpi
153-
self.figure.set_size_inches(width / dpi, height / dpi, forward=False)
166+
winch = width * self.device_pixel_ratio / dpi
167+
hinch = height * self.device_pixel_ratio / dpi
168+
self.figure.set_size_inches(winch, hinch, forward=False)
154169
FigureCanvasBase.resize_event(self)
155170
self.draw_idle()
156171

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

189+
def _update_device_pixel_ratio(self, *args, **kwargs):
190+
# We need to be careful in cases with mixed resolution displays if
191+
# device_pixel_ratio changes.
192+
if self._set_device_pixel_ratio(self.get_scale_factor()):
193+
self.draw()
194+
174195
def _draw_rubberband(self, rect):
175196
self._rubberband_rect = rect
176197
# TODO: Only update the rubberband area.
@@ -184,7 +205,8 @@ def _post_draw(self, widget, ctx):
184205
if self._rubberband_rect is None:
185206
return
186207

187-
x0, y0, w, h = self._rubberband_rect
208+
x0, y0, w, h = (dim / self.device_pixel_ratio
209+
for dim in self._rubberband_rect)
188210
x1 = x0 + w
189211
y1 = y0 + h
190212

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._renderer.dpi = self.figure.dpi
3134
self.figure.draw(self._renderer)
3235

0 commit comments

Comments
 (0)
0