Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions lib/matplotlib/backends/_backend_gtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,11 @@
TimerBase)
from matplotlib.backend_tools import Cursors

import gi
# The GTK3/GTK4 backends will have already called `gi.require_version` to set
# the desired GTK.
from gi.repository import Gdk, Gio, GLib, Gtk, GdkPixbuf


try:
gi.require_foreign("cairo")
except ImportError as e:
raise ImportError("Gtk-based backends require cairo") from e

_log = logging.getLogger(__name__)
_application = None # Placeholder

Expand Down
10 changes: 9 additions & 1 deletion lib/matplotlib/backends/backend_gtk3.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)

try:
from gi import require_version as gi_require_version
from gi import (
require_version as gi_require_version,
require_foreign as gi_require_foreign,
)
except ImportError as err:
raise ImportError("The GTK3 backends require PyGObject") from err

Expand All @@ -24,6 +27,11 @@
# auto-backend selection logic correctly skips.
raise ImportError(e) from e

try:
gi_require_foreign("cairo")
except ImportError as e:
raise ImportError("GTK3-based backends require cairo") from e

from gi.repository import Gio, GLib, GObject, Gtk, Gdk
from . import _backend_gtk
from ._backend_gtk import ( # noqa: F401 # pylint: disable=W0611
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/backends/backend_gtk3agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from . import backend_agg, backend_gtk3
from .backend_gtk3 import GLib, Gtk, _BackendGTK3

import cairo # Presence of cairo is already checked by _backend_gtk.
import cairo # Presence of cairo is already checked by backend_gtk3.


class FigureCanvasGTK3Agg(backend_agg.FigureCanvasAgg,
Expand Down
117 changes: 72 additions & 45 deletions lib/matplotlib/backends/backend_gtk4.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,39 @@
gi_require_version("Gtk", "4.0")
gi_require_version("Gdk", "4.0")
gi_require_version("GdkPixbuf", "2.0")
gi_require_version("Graphene", "1.0")
except ValueError as e:
# in this case we want to re-raise as ImportError so the
# auto-backend selection logic correctly skips.
raise ImportError(e) from e

import gi
from gi.repository import Gio, GLib, Gtk, Gdk, GdkPixbuf
from gi.repository import GLib, Gio, Gdk, GdkPixbuf, Graphene, Gtk
from . import _backend_gtk
from ._backend_gtk import ( # noqa: F401 # pylint: disable=W0611
_BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
TimerGTK as TimerGTK4,
)

# With a new enough version, we can use nearest scaling instead of linear, which helps
# preserve sharpness on fractional HiDPI displays.
_USE_SCALED_TEXTURE = Gtk.check_version(4, 10, 0) is None
# For GTK 4.14, there is enough path implementation to use the snapshot for the zoom
# tool, and then we don't need Cairo.
_USE_SNAPSHOT_ZOOM = Gtk.check_version(4, 14, 0) is None
if _USE_SNAPSHOT_ZOOM or _USE_SCALED_TEXTURE:
from gi.repository import Gsk
else:
try:
gi.require_foreign("cairo")
except ImportError as e:
raise ImportError("Gtk-based backends require cairo") from e

_GOBJECT_GE_3_47 = gi.version_info >= (3, 47, 0)
_GTK_GE_4_12 = Gtk.check_version(4, 12, 0) is None


class FigureCanvasGTK4(_FigureCanvasGTK, Gtk.DrawingArea):
class FigureCanvasGTK4(_FigureCanvasGTK, Gtk.Widget):
required_interactive_framework = "gtk4"
supports_blit = False
manager_class = _api.classproperty(lambda cls: FigureManagerGTK4)
Expand All @@ -50,8 +65,6 @@ def __init__(self, figure=None):
self._idle_draw_id = 0
self._rubberband_rect = None

self.set_draw_func(self._draw_func)
self.connect('resize', self.resize_event)
if _GTK_GE_4_12:
self.connect('realize', self._realize_event)
else:
Expand Down Expand Up @@ -81,16 +94,6 @@ def __init__(self, figure=None):

self.set_focusable(True)

css = Gtk.CssProvider()
style = '.matplotlib-canvas { background-color: white; }'
if Gtk.check_version(4, 9, 3) is None:
css.load_from_data(style, -1)
else:
css.load_from_data(style.encode('utf-8'))
style_ctx = self.get_style_context()
style_ctx.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
style_ctx.add_class("matplotlib-canvas")

def destroy(self):
CloseEvent("close_event", self)._process()

Expand Down Expand Up @@ -183,7 +186,7 @@ def leave_notify_event(self, controller):
guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
)._process()

def resize_event(self, area, width, height):
def do_size_allocate(self, width, height, baseline):
self._update_device_pixel_ratio()
dpi = self.figure.dpi
winch = width * self.device_pixel_ratio / dpi
Expand Down Expand Up @@ -265,11 +268,15 @@ def _draw_rubberband(self, rect):
# TODO: Only update the rubberband area.
self.queue_draw()

def _draw_func(self, drawing_area, ctx, width, height):
self.on_draw_event(self, ctx)
self._post_draw(self, ctx)
def do_snapshot(self, snapshot):
white = Gdk.RGBA()
white.red = white.green = white.blue = white.alpha = 1.0
rect = Graphene.Rect().init(0, 0, self.get_width(), self.get_height())
snapshot.append_color(white, rect)
Comment thread
iccir marked this conversation as resolved.
Resolved
self.on_snapshot_event(snapshot)
self._post_snapshot(snapshot)

def _post_draw(self, widget, ctx):
def _post_snapshot(self, snapshot):
if self._rubberband_rect is None:
return

Expand All @@ -280,28 +287,49 @@ def _post_draw(self, widget, ctx):
x1 = x0 + w
y1 = y0 + h

# Draw the lines from x0, y0 towards x1, y1 so that the
# dashes don't "jump" when moving the zoom box.
ctx.move_to(x0, y0)
ctx.line_to(x0, y1)
ctx.move_to(x0, y0)
ctx.line_to(x1, y0)
ctx.move_to(x0, y1)
ctx.line_to(x1, y1)
ctx.move_to(x1, y0)
ctx.line_to(x1, y1)

ctx.set_antialias(1)
ctx.set_line_width(lw)
ctx.set_dash((dash, dash), 0)
ctx.set_source_rgb(0, 0, 0)
ctx.stroke_preserve()

ctx.set_dash((dash, dash), dash)
ctx.set_source_rgb(1, 1, 1)
ctx.stroke()

def on_draw_event(self, widget, ctx):
def make_path(builder):
# Draw the lines from x0, y0 towards x1, y1 so that the dashes don't "jump"
# when moving the zoom box.
builder.move_to(x0, y0)
builder.line_to(x0, y1)
builder.move_to(x0, y0)
builder.line_to(x1, y0)
builder.move_to(x0, y1)
builder.line_to(x1, y1)
builder.move_to(x1, y0)
builder.line_to(x1, y1)

if _USE_SNAPSHOT_ZOOM:
pb = Gsk.PathBuilder()
make_path(pb)
path = pb.to_path()

stroke = Gsk.Stroke(line_width=lw)
stroke.set_dash((dash, dash))
colour = Gdk.RGBA()
colour.alpha = 1.0
colour.red = colour.green = colour.blue = 0.0
snapshot.append_stroke(path, stroke, colour)

stroke.set_dash_offset(dash)
colour.red = colour.green = colour.blue = 1.0
snapshot.append_stroke(path, stroke, colour)
else:
rect = Graphene.Rect().init(0, 0, self.get_width(), self.get_height())
ctx = snapshot.append_cairo(rect)
make_path(ctx)

ctx.set_antialias(1)
ctx.set_line_width(lw)
ctx.set_dash((dash, dash), 0)
ctx.set_source_rgb(0, 0, 0)
ctx.stroke_preserve()

ctx.set_dash((dash, dash), dash)
ctx.set_source_rgb(1, 1, 1)
ctx.stroke()

def on_snapshot_event(self, snapshot):
# to be overwritten by GTK4Agg or GTK4Cairo
pass

Expand Down Expand Up @@ -340,10 +368,9 @@ def __init__(self, canvas):
if text is None:
self.append(Gtk.Separator())
continue
image = Gtk.Image.new_from_gicon(
Gio.Icon.new_for_string(
str(cbook._get_data_path('images',
f'{image_file}-symbolic.svg'))))
path = str(cbook._get_data_path('images', f'{image_file}-symbolic.svg'))
icon = Gtk.IconPaintable.new_for_file(Gio.File.new_for_path(path), 24, 1)
image = Gtk.Picture.new_for_paintable(icon)
self._gtk_ids[text] = button = (
Gtk.ToggleButton() if callback in ['zoom', 'pan'] else
Gtk.Button())
Expand Down
60 changes: 37 additions & 23 deletions lib/matplotlib/backends/backend_gtk4agg.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,53 @@
import numpy as np
from math import ceil, floor

from .. import cbook
from . import backend_agg, backend_gtk4
from .backend_gtk4 import GLib, Gtk, _BackendGTK4
from .backend_gtk4 import GLib, Gdk, Graphene, _BackendGTK4, _USE_SCALED_TEXTURE

import cairo # Presence of cairo is already checked by _backend_gtk.
if _USE_SCALED_TEXTURE:
from .backend_gtk4 import Gsk


class FigureCanvasGTK4Agg(backend_agg.FigureCanvasAgg,
backend_gtk4.FigureCanvasGTK4):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._texture = None

def on_draw_event(self, widget, ctx):
def on_snapshot_event(self, snapshot):
if self._idle_draw_id:
GLib.source_remove(self._idle_draw_id)
self._idle_draw_id = 0
self.draw()

scale = self.device_pixel_ratio
allocation = self.get_allocation()

Gtk.render_background(
self.get_style_context(), ctx,
allocation.x, allocation.y,
allocation.width, allocation.height)

buf = cbook._unmultiplied_rgba8888_to_premultiplied_argb32(
np.asarray(self.get_renderer().buffer_rgba()))
height, width, _ = buf.shape
image = cairo.ImageSurface.create_for_data(
buf.ravel().data, cairo.FORMAT_ARGB32, width, height)
image.set_device_scale(scale, scale)
ctx.set_source_surface(image, 0, 0)
ctx.paint()

return False
view = self.get_renderer().buffer_rgba()
buf = GLib.Bytes.new(bytes(view))
self._texture = Gdk.MemoryTexture.new(view.shape[1], view.shape[0],
Gdk.MemoryFormat.R8G8B8A8,
buf, view.strides[0])

width = self.get_width()
height = self.get_height()
# Yes, Graphene.Rect really does have this strange initialization API.
area = Graphene.Rect()
Graphene.Rect.init(
area,
# Snap the texture to a physical pixel so it is not blurred.
1 - ceil(self.device_pixel_ratio) / self.device_pixel_ratio,
1 - ceil(self.device_pixel_ratio) / self.device_pixel_ratio,
ceil(width * self.device_pixel_ratio),
ceil(height * self.device_pixel_ratio))

snapshot.save()
snapshot.scale(1 / self.device_pixel_ratio, 1 / self.device_pixel_ratio)

if (_USE_SCALED_TEXTURE and
self._texture.get_height() == floor(area.size.height)):
snapshot.append_scaled_texture(self._texture, Gsk.ScalingFilter.NEAREST,
area)
else:
snapshot.append_texture(self._texture, area)

snapshot.restore()


@_BackendGTK4.export
Expand Down
13 changes: 6 additions & 7 deletions lib/matplotlib/backends/backend_gtk4cairo.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from contextlib import nullcontext

from .backend_cairo import FigureCanvasCairo
from .backend_gtk4 import GLib, Gtk, FigureCanvasGTK4, _BackendGTK4
from .backend_gtk4 import GLib, Graphene, FigureCanvasGTK4, _BackendGTK4


class FigureCanvasGTK4Cairo(FigureCanvasCairo, FigureCanvasGTK4):
Expand All @@ -10,20 +10,19 @@ def _set_device_pixel_ratio(self, ratio):
# changes to the device pixel ratio.
return False

def on_draw_event(self, widget, ctx):
def on_snapshot_event(self, snapshot):
if self._idle_draw_id:
GLib.source_remove(self._idle_draw_id)
self._idle_draw_id = 0
self.draw()

with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
else nullcontext()):
self._renderer.set_context(ctx)
allocation = self.get_allocation()
Gtk.render_background(
self.get_style_context(), ctx,
allocation.x, allocation.y,
allocation.width, allocation.height)
bounds = Graphene.Rect().init(allocation.x, allocation.y,
allocation.width, allocation.height)
ctx = snapshot.append_cairo(bounds)
self._renderer.set_context(ctx)
self.figure.draw(self._renderer)


Expand Down
Loading