[vte] all: Initial port for gtk4



commit 075d0b37049244429b7d709dc8c21f67023bc048
Author: Christian Persch <chpe src gnome org>
Date:   Sun Feb 21 20:05:43 2021 +0100

    all: Initial port for gtk4
    
    Add meson changes to build a gtk4 variant of libvte, plus
    a gtk4 variant of the test application.
    
    Make the minimal code changes required to successfully build
    and run on gtk4.
    
    No event handlers yet, and no public API to replace the gtk3
    specific APIs.  That will come later.
    
    This should be good enough to get non-terminal users of libvte
    started on porting to gtk4.
    
    https://gitlab.gnome.org/GNOME/vte/-/issues/12

 Makefile.meson                                     |  18 +-
 bindings/gir/meson.build                           |  35 +-
 bindings/glade/meson.build                         |   2 +-
 doc/reference/Makefile.docs                        | 518 ++++++++++++++++
 doc/reference/gtk3/meson.build                     |  55 ++
 doc/reference/gtk4/meson.build                     |  55 ++
 doc/reference/meson.build                          |  94 +--
 doc/reference/version.xml.in                       |   1 -
 doc/reference/vte-docs.xml                         |  16 +-
 .../{vte-overrides.txt => vte-overrides.txt.in}    |   0
 .../{vte-sections.txt => vte-sections.txt.in}      |  12 +
 doc/reference/{vte.types => vte.types.in}          |   0
 meson.build                                        |  36 +-
 po/POTFILES.skip                                   |   6 +-
 src/app/app-gtk4.gresource.xml                     |  24 +
 src/app/app.cc                                     | 676 +++++++++++++++++----
 src/app/appmenu-gtk4.ui                            |  33 +
 src/app/meson.build                                |  33 +-
 src/app/search-popover-gtk3.ui                     |   1 -
 src/app/search-popover-gtk4.ui                     | 169 ++++++
 src/app/window-gtk3.ui                             |   1 -
 src/app/window-gtk4.ui                             | 185 ++++++
 src/cairo-glue.hh                                  |   4 +-
 src/clipboard-gtk.cc                               |  39 +-
 src/clipboard-gtk.hh                               |   4 +
 src/debug.h                                        |   6 +
 src/fonts-pangocairo.cc                            |  32 +-
 src/fonts-pangocairo.hh                            |   5 +-
 src/graphene-glue.hh                               |  48 ++
 src/gtk-glue.hh                                    |   6 +
 src/keymap.h                                       |   5 +
 src/meson.build                                    | 104 +++-
 src/vte.cc                                         | 362 +++++++----
 src/vte/meson.build                                | 107 +++-
 src/vte/vte.h                                      |   4 +
 src/vte/vtedeprecated.h                            |  24 +-
 src/vte/vtemacros.h                                |  20 +-
 src/vte/vtepty.h                                   |  10 +-
 src/vte/vteregex.h                                 |   2 +-
 src/vte/vteterminal.h                              |  41 +-
 src/vte/vtetypebuiltins.h                          |  28 +
 src/vteaccess.h                                    |   6 +-
 src/vtedefines.hh                                  |   3 +
 src/vtegtk.cc                                      | 404 ++++++++++--
 src/vteinternal.hh                                 |  71 ++-
 src/vteseq.cc                                      |   5 +
 src/widget.cc                                      | 559 +++++++++++++++--
 src/widget.hh                                      | 111 +++-
 48 files changed, 3398 insertions(+), 582 deletions(-)
---
diff --git a/Makefile.meson b/Makefile.meson
index 4917fb23..40bbdb42 100644
--- a/Makefile.meson
+++ b/Makefile.meson
@@ -15,8 +15,7 @@
 
 srcdir=@srcdir@
 builddir=@builddir@
-vte_gtk3_api_version = @vte_gtk3_api_version@
-vte_gtk4_api_version = @vte_gtk4_api_version@
+vte_api_version = @vte_api_version@
 
 #
 
@@ -29,6 +28,18 @@ NINJA = ninja $(NJOBS)
 all:
        $(NINJA)
 
+gtk3:
+       $(NINJA) src/app/vte-$(vte_api_version)
+
+gtk4:
+       $(NINJA) src/app/vte-$(vte_api_version)-gtk4
+
+doc-gtk3:
+       $(NINJA) doc/reference/gtk3/meson.stamp
+
+doc-gtk4:
+       $(NINJA) doc/reference/gtk4/meson.stamp
+
 check:
        MESON_TESTTHREADS=$(NTHREADS) $(NINJA) test
 
@@ -38,8 +49,7 @@ clean:
 coverage:
        $(NINJA) coverage
 
-doc:
-       $(NINJA) vte-$(vte_gtk3_api_version)-doc
+doc: doc-gtk3 doc-gtk4
 
 install:
        $(NINJA) install
diff --git a/bindings/gir/meson.build b/bindings/gir/meson.build
index 3793a430..d5f77fc6 100644
--- a/bindings/gir/meson.build
+++ b/bindings/gir/meson.build
@@ -1,5 +1,5 @@
 # Copyright © 2018, 2019 Iñigo Martínez
-# Copyright © 2019 Christian Persch
+# Copyright © 2019, 2020, 2021 Christian Persch
 #
 # This library is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Lesser General Public License as published
@@ -18,6 +18,7 @@ gir_dep = dependency('gobject-introspection-1.0', version: '>= 0.9.0')
 
 if get_option('gtk3')
   libvte_gtk3_gir_includes = [
+    'cairo-1.0',
     'Gdk-3.0',
     'Gtk-3.0',
     'Pango-1.0',
@@ -29,10 +30,40 @@ if get_option('gtk3')
     includes: libvte_gtk3_gir_includes,
     dependencies: libvte_gtk3_dep,
     extra_args: '-DVTE_COMPILATION',
-    nsversion: vte_gtk3_api_version,
+    nsversion: vte_api_version,
     namespace: 'Vte',
     export_packages: vte_gtk3_api_name,
     header: 'vte' / 'vte.h',
     install: true,
   )
 endif
+
+if get_option('gtk4')
+  libvte_gtk4_gir_includes = [
+    'cairo-1.0',
+    'Graphene-1.0',
+    'Gsk-4.0',
+    'Gdk-4.0',
+    'Gtk-4.0',
+    'Pango-1.0',
+  ]
+
+  # Ideally, the gir would be named something like "VteGtk4" instead,
+  # but it seems that's not possible. So work around it using "Vte"
+  # as namespace with this nsversion hack:
+  gir_nsversion_gtk4 = '4:' + vte_api_version
+
+  libvte_gtk4_gir = gnome.generate_gir(
+    libvte_gtk4,
+    dependencies: libvte_gtk4_dep,
+    export_packages: vte_gtk4_api_name,
+    extra_args: '-DVTE_COMPILATION',
+    header: 'vte' / 'vte.h',
+    includes: libvte_gtk4_gir_includes,
+    install: true,
+    namespace: 'Vte',
+    nsversion: gir_nsversion_gtk4,
+    sources: libvte_gtk4_public_headers + libvte_common_doc_sources,
+    symbol_prefix: 'vte',
+  )
+endif
diff --git a/bindings/glade/meson.build b/bindings/glade/meson.build
index 90d4672e..6bca2aea 100644
--- a/bindings/glade/meson.build
+++ b/bindings/glade/meson.build
@@ -19,7 +19,7 @@ cataloguedir =  gladedir / 'catalogs'
 pixmapdir = gladedir / 'pixmaps'
 
 catalog_conf = configuration_data()
-catalog_conf.set('VTE_API_VERSION', vte_gtk3_api_version)
+catalog_conf.set('VTE_API_VERSION', vte_api_version)
 catalog_conf.set('VERSION', vte_version)
 
 configure_file(
diff --git a/doc/reference/Makefile.docs b/doc/reference/Makefile.docs
new file mode 100644
index 00000000..7193bd16
--- /dev/null
+++ b/doc/reference/Makefile.docs
@@ -0,0 +1,518 @@
+# -*- mode: makefile -*-
+#
+# Copyright © 2020, 2021 Christian Persch
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+NULL =
+V ?= 0
+
+abs_srcdir ?= $(srcdir)
+abs_builddir ?= $(builddir)
+top_srcdir ?= $(abs_top_srcdir)
+top_builddir ?= $(abs_top_builddir)
+
+datadir ?= /usr/share
+
+CPP = cpp
+CPPFLAGS =
+
+GREP ?= grep
+GREPFLAGS =
+
+LN_S = ln -s
+
+PACKAGE ?= vte
+PACKAGE_BUGREPORT ?= https://gitlab.gnome.org/GNOME/vte/issues/
+PACKAGE_NAME ?= vte
+PACKAGE_STRING ?= vte
+PACKAGE_TARNAME ?= vte
+PACKAGE_URL ?= https://gitlab.gnome.org/GNOME/vte/
+PACKAGE_VERSION ?= $(VERSION)
+
+DOC_MODULE = vte-gtk$(VTE_GTK)
+
+DOC_MODULE_VERSION = $(VTE_API_VERSION)
+
+DOC_MAIN_SGML_FILE = $(DOC_MODULE)-docs.xml
+
+DOC_SOURCE_DIR = \
+       $(top_srcdir)/src \
+       $(top_srcdir)/src/vte \
+       $(top_builddir)/src \
+       $(top_builddir)/src/vte \
+       $(NULL)
+
+SCANGOBJ_OPTIONS =
+
+SCAN_OPTIONS = \
+       --deprecated-guards="VTE_DISABLE_DEPRECATED" \
+       --ignore-decorators='_VTE_GNUC_NONNULL()|_VTE_PUBLIC|_VTE_DEPRECATED|_VTE_CXX_NOEXCEPT' \
+       $(NULL)
+
+MKDB_OPTIONS = \
+       --source-suffixes=c,cc,h,hh \
+       --xml-mode \
+       --output-format=xml \
+       --name-space=vte \
+       $(NULL)
+
+MKTMPL_OPTIONS =
+
+MKHTML_OPTIONS = \
+       --path="$(abs_builddir)" \
+       $(NULL)
+
+MKPDF_OPTIONS = \
+       --path="$(abs_builddir)" \
+       $(NULL)
+
+FIXXREF_OPTIONS = \
+       --extra-dir=$(CAIRO_PREFIX)/share/gtk-doc/html/cairo \
+       --extra-dir=$(GLIB_PREFIX)/share/gtk-doc/html/glib \
+       --extra-dir=$(GLIB_PREFIX)/share/gtk-doc/html/gobject \
+       --extra-dir=$(GLIB_PREFIX)/share/gtk-doc/html/gio \
+       --extra-dir=$(PANGO_PREFIX)/share/gtk-doc/html/pango \
+       $(NULL)
+
+ifeq ($(VTE_GTK),3)
+FIXXREF_OPTIONS += \
+       --extra-dir=$(GTK_PREFIX)/share/gtk-doc/html/gdk3 \
+       --extra-dir=$(GTK_PREFIX)/share/gtk-doc/html/gtk3 \
+       $(NULL)
+endif
+
+ifeq ($(VTE_GTK),4)
+FIXXREF_OPTIONS += \
+       --extra-dir=$(GTK_PREFIX)/share/gtk-doc/html/graphene \
+       --extra-dir=$(GTK_PREFIX)/share/gtk-doc/html/gdk4 \
+       --extra-dir=$(GTK_PREFIX)/share/gtk-doc/html/gsk4 \
+       --extra-dir=$(GTK_PREFIX)/share/gtk-doc/html/gtk4 \
+       $(NULL)
+endif
+
+HFILE_GLOB = \
+       $(top_builddir)/src/vte/*.h \
+       $(top_srcdir)/src/vte/*.h \
+       $(NULL)
+
+CFILE_GLOB = \
+       $(top_builddir)/src/*.c \
+       $(top_srcdir)/src/*.c \
+       $(top_srcdir)/src/*.cc \
+       $(NULL)
+
+EXTRA_HFILES =
+
+IGNORE_HFILES = \
+       buffer.h \
+       caps.hh \
+       cell.hh \
+       config.h \
+       debug.h \
+       keymap.h \
+       marshal.h \
+       modes.hh \
+       modes-ecma.hh \
+       modes-private.hh \
+       parser.hh \
+       parser-arg.hh \
+       parser-c01.hh \
+       parser-charset.hh \
+       parser-charset-tables.hh \
+       parser-cmd.hh \
+       parser-csi.hh \
+       parser-dcs.hh \
+       parser-esc.hh \
+       parser-glue.hh \
+       parser-osc.hh \
+       parser-reply.hh \
+       parser-string.hh \
+       ring.hh \
+       tabstops.hh \
+       vteconv.h \
+       vtedraw.h \
+       vteinternal.hh \
+       vterowdata.hh \
+       vtestream-base.h \
+       vtestream-file.h \
+       vtestream.h \
+       vtetypebuiltins.h \
+       vteunistr.h \
+       $(NULL)
+
+HTML_IMAGES =
+
+content_files =
+
+expand_content_files =
+
+GTKDOC_CFLAGS = \
+       -DVTE_COMPILATION \
+       $(shell pkg-config --cflags --libs glib-2.0 gobject-2.0) \
+       $(NULL)
+
+VTE_LIB_PATH = $(shell dirname $(VTE_LIB))
+
+ifeq ($(VTE_GTK),3)
+VTE_LIB_NAME = vte-$(VTE_API_VERSION)
+endif
+ifeq ($(VTE_GTK),4)
+VTE_LIB_NAME = vte-$(VTE_API_VERSION)-gtk4
+endif
+
+GTKDOC_LIBS = \
+       -L$(VTE_LIB_PATH) -l$(VTE_LIB_NAME) \
+       $(shell pkg-config --libs --libs glib-2.0 gobject-2.0) \
+       $(NULL)
+
+# Rules for building gtk3/4 versions of the gtk-doc inputs
+
+AM_V_at = $(AM_V_at_$(V))
+AM_V_at_0 = @
+AM_V_at_1 =
+
+AM_V_GEN = $(AM_V_GEN_$(V))
+AM_V_GEN_0 = @echo "  GEN  " $@;
+AM_V_GEN_1 =
+
+vte-gtk$(VTE_GTK)-sections.txt: $(srcdir)/../vte-sections.txt.in
+       $(AM_V_GEN)$(CPP) -E $(CPPFLAGS) -DVTE_GTK=$(VTE_GTK) $< | $(GREP) $(GREPFLAGS) -Ev '^\s*#|^$$' > $@
+
+vte-gtk$(VTE_GTK)-overrides.txt: $(srcdir)/../vte-overrides.txt.in
+       $(AM_V_GEN)$(CPP) -E $(CPPFLAGS) -DVTE_GTK=$(VTE_GTK) $< | $(GREP) $(GREPFLAGS) -Ev '^\s*#|^$$' > $@ 
|| true
+
+vte-gtk$(VTE_GTK).types: $(srcdir)/../vte.types.in
+       $(AM_V_GEN)$(CPP) -E -fpreprocessed $(CPPFLAGS) -DVTE_GTK=$(VTE_GTK) $< | $(GREP) $(GREPFLAGS) -Ev 
'^\s*#|^$$' > $@
+
+$(DOC_MAIN_SGML_FILE): $(srcdir)/../vte-docs.xml
+       $(AM_V_GEN)cp -f $< $@
+
+# The following is copied from gtk-doc, and adapted to work with
+# plain make instead of requiring automake.
+#
+# Copyright (C) 2003 James Henstridge
+#               2004-2007 Damon Chaplin
+#               2007-2017 Stefan Sauer
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+####################################
+# Everything below here is generic #
+####################################
+
+CC ?= cc
+CFLAGS ?=
+
+INSTALL = install -c
+INSTALL_DATA = $(INSTALL) -m 644
+
+GTKDOC_CC = $(CC) $(INCLUDES) $(GTKDOC_DEPS_CFLAGS) $(CPPFLAGS) $(CFLAGS)
+GTKDOC_LD = $(CC) $(GTKDOC_DEPS_LIBS) $(CFLAGS) $(LDFLAGS)
+GTKDOC_RUN =
+
+GTKDOC_CHECK_PATH = gtkdoc-check
+GTKDOC_REBASE = gtkdoc-rebase
+
+MKDIR_P ?= mkdir -p
+
+# We set GPATH here; this gives us semantics for GNU make
+# which are more like other make's VPATH, when it comes to
+# whether a source that is a target of one rule is then
+# searched for in VPATH/GPATH.
+#
+GPATH = $(srcdir)
+
+HTML_DIR = $(datadir)/gtk-doc/html
+
+TARGET_DIR=$(HTML_DIR)/$(DOC_MODULE)
+
+SETUP_FILES = \
+       $(content_files)                \
+       $(expand_content_files)         \
+       $(DOC_MAIN_SGML_FILE)           \
+       $(DOC_MODULE)-sections.txt      \
+       $(DOC_MODULE)-overrides.txt
+
+SETUP_FILES_GENERATED = \
+       $(DOC_MODULE)-sections.txt      \
+       $(DOC_MODULE)-overrides.txt     \
+       $(DOC_MODULE).types             \
+       $(DOC_MAIN_SGML_FILE)
+
+EXTRA_DIST =                           \
+       $(HTML_IMAGES)                  \
+       $(SETUP_FILES)
+
+DOC_STAMPS=setup-build.stamp scan-build.stamp sgml-build.stamp \
+       html-build.stamp pdf-build.stamp \
+       sgml.stamp html.stamp pdf.stamp
+
+SCANOBJ_FILES =                 \
+       $(DOC_MODULE).actions    \
+       $(DOC_MODULE).args       \
+       $(DOC_MODULE).hierarchy  \
+       $(DOC_MODULE).interfaces \
+       $(DOC_MODULE).prerequisites \
+       $(DOC_MODULE).signals
+
+REPORT_FILES = \
+       $(DOC_MODULE)-undocumented.txt \
+       $(DOC_MODULE)-undeclared.txt \
+       $(DOC_MODULE)-unused.txt
+
+gtkdoc-check.test:
+       $(AM_V_GEN)echo "#!/bin/sh -e" > $@; \
+               echo "$(GTKDOC_CHECK_PATH) || exit 1" >> $@; \
+               chmod +x $@
+
+CLEANFILES = $(SCANOBJ_FILES) $(REPORT_FILES) $(DOC_STAMPS) gtkdoc-check.test
+
+HTML_BUILD_STAMP=html-build.stamp
+#PDF_BUILD_STAMP=pdf-build.stamp
+PDF_BUILD_STAMP=
+
+all-gtk-doc: $(HTML_BUILD_STAMP) $(PDF_BUILD_STAMP)
+.PHONY: all-gtk-doc
+
+all-local: all-gtk-doc
+
+docs: $(HTML_BUILD_STAMP) $(PDF_BUILD_STAMP)
+
+$(REPORT_FILES): sgml-build.stamp
+
+#### setup ####
+
+GTK_DOC_V_SETUP=$(GTK_DOC_V_SETUP_$(V))
+GTK_DOC_V_SETUP_0=@echo "  DOC   Preparing build";
+GTK_DOC_V_SETUP_1=
+
+setup-build.stamp: $(SETUP_FILES_GENERATED)
+       -$(GTK_DOC_V_SETUP)if test "$(abs_srcdir)" != "$(abs_builddir)" ; then \
+         files=`echo $(SETUP_FILES) $(DOC_MODULE).types`; \
+         if test "x$$files" != "x" ; then \
+           for file in $$files ; do \
+             destdir=`dirname $(abs_builddir)/$$file`; \
+             test -d "$$destdir" || $(MKDIR_P) "$$destdir"; \
+             test -f $(abs_srcdir)/$$file && \
+               cp -pf $(abs_srcdir)/$$file $(abs_builddir)/$$file || true; \
+           done; \
+         fi; \
+       fi
+       $(AM_V_at)touch setup-build.stamp
+
+#### scan ####
+
+GTK_DOC_V_SCAN=$(GTK_DOC_V_SCAN_$(V))
+GTK_DOC_V_SCAN_0=@echo "  DOC   Scanning header files";
+GTK_DOC_V_SCAN_1=
+
+GTK_DOC_V_INTROSPECT=$(GTK_DOC_V_INTROSPECT_$(V))
+GTK_DOC_V_INTROSPECT_0=@echo "  DOC   Introspecting gobjects";
+GTK_DOC_V_INTROSPECT_1=
+
+scan-build.stamp: setup-build.stamp $(HFILE_GLOB) $(CFILE_GLOB)
+       $(GTK_DOC_V_SCAN)_source_dir='' ; \
+       for i in $(DOC_SOURCE_DIR) ; do \
+         _source_dir="$${_source_dir} --source-dir=$$i" ; \
+       done ; \
+       gtkdoc-scan --module=$(DOC_MODULE) --ignore-headers="$(IGNORE_HFILES)" $${_source_dir} 
$(SCAN_OPTIONS) $(EXTRA_HFILES)
+       $(GTK_DOC_V_INTROSPECT)if grep -l '^..*$$' $(DOC_MODULE).types > /dev/null 2>&1 ; then \
+         scanobj_options=""; \
+         gtkdoc-scangobj 2>&1 --help | grep  >/dev/null "\-\-verbose"; \
+         if test "$$?" = "0"; then \
+           if test "x$(V)" = "x1"; then \
+             scanobj_options="--verbose"; \
+           fi; \
+         fi; \
+         CC="$(GTKDOC_CC)" LD="$(GTKDOC_LD)" RUN="$(GTKDOC_RUN)" CFLAGS="$(GTKDOC_CFLAGS) $(CFLAGS)" 
LDFLAGS="$(GTKDOC_LIBS) $(LDFLAGS)" LD_LIBRARY_PATH="$(VTE_LIB_PATH)" \
+         gtkdoc-scangobj $(SCANGOBJ_OPTIONS) $$scanobj_options --module=$(DOC_MODULE); \
+       else \
+         for i in $(SCANOBJ_FILES) ; do \
+           test -f $$i || touch $$i ; \
+         done \
+       fi
+       $(AM_V_at)touch scan-build.stamp
+
+$(DOC_MODULE)-decl.txt $(SCANOBJ_FILES): scan-build.stamp
+       @true
+
+#### xml ####
+
+GTK_DOC_V_XML=$(GTK_DOC_V_XML_$(V))
+GTK_DOC_V_XML_0=@echo "  DOC   Building XML";
+GTK_DOC_V_XML_1=
+
+sgml-build.stamp: setup-build.stamp $(DOC_MODULE)-decl.txt $(SCANOBJ_FILES) $(HFILE_GLOB) $(CFILE_GLOB) 
$(DOC_MODULE)-sections.txt $(DOC_MODULE)-overrides.txt $(expand_content_files) xml/gtkdocentities.ent 
$(DOC_MAIN_SGML_FILE)
+       $(GTK_DOC_V_XML)_source_dir='' ; \
+       for i in $(DOC_SOURCE_DIR) ; do \
+         _source_dir="$${_source_dir} --source-dir=$$i" ; \
+       done ; \
+       gtkdoc-mkdb --module=$(DOC_MODULE) --output-format=xml 
--expand-content-files="$(expand_content_files)" --main-sgml-file=$(DOC_MAIN_SGML_FILE) $${_source_dir} 
$(MKDB_OPTIONS)
+       $(AM_V_at)touch sgml-build.stamp
+
+sgml.stamp: sgml-build.stamp
+       @true
+
+xml/gtkdocentities.ent:
+       $(GTK_DOC_V_XML)$(MKDIR_P) $(@D) && ( \
+               echo "<!ENTITY package \"$(PACKAGE)\">"; \
+               echo "<!ENTITY package_bugreport \"$(PACKAGE_BUGREPORT)\">"; \
+               echo "<!ENTITY package_name \"$(PACKAGE_NAME)\">"; \
+               echo "<!ENTITY package_string \"$(PACKAGE_STRING)\">"; \
+               echo "<!ENTITY package_tarname \"$(PACKAGE_TARNAME)\">"; \
+               echo "<!ENTITY package_url \"$(PACKAGE_URL)\">"; \
+               echo "<!ENTITY package_version \"$(PACKAGE_VERSION)\">"; \
+       ) > $@
+
+
+#### html ####
+
+GTK_DOC_V_HTML=$(GTK_DOC_V_HTML_$(V))
+GTK_DOC_V_HTML_0=@echo "  DOC   Building HTML";
+GTK_DOC_V_HTML_1=
+
+GTK_DOC_V_XREF=$(GTK_DOC_V_XREF_$(V))
+GTK_DOC_V_XREF_0=@echo "  DOC   Fixing cross-references";
+GTK_DOC_V_XREF_1=
+
+GTKDOC_MKHTML = gtkdoc-mkhtml
+
+html-build.stamp: sgml.stamp $(DOC_MAIN_SGML_FILE) $(content_files) $(expand_content_files)
+       $(GTK_DOC_V_HTML)rm -rf html && mkdir html && \
+       mkhtml_options=""; \
+       $(GTKDOC_MKHTML) 2>&1 --help | grep  >/dev/null "\-\-verbose"; \
+       if test "$$?" = "0"; then \
+         if test "x$(V)" = "x1"; then \
+           mkhtml_options="$$mkhtml_options --verbose"; \
+         fi; \
+       fi; \
+       $(GTKDOC_MKHTML) 2>&1 --help | grep  >/dev/null "\-\-path"; \
+       if test "$$?" = "0"; then \
+         mkhtml_options="$$mkhtml_options --path=\"$(abs_srcdir)\""; \
+       fi; \
+       cd html && $(GTKDOC_MKHTML) $$mkhtml_options $(MKHTML_OPTIONS) $(DOC_MODULE) ../$(DOC_MAIN_SGML_FILE)
+       -@test "x$(HTML_IMAGES)" = "x" || \
+       for file in $(HTML_IMAGES) ; do \
+         test -f $(abs_srcdir)/$$file && cp $(abs_srcdir)/$$file $(abs_builddir)/html; \
+         test -f $(abs_builddir)/$$file && cp $(abs_builddir)/$$file $(abs_builddir)/html; \
+         test -f $$file && cp $$file $(abs_builddir)/html; \
+       done;
+       $(GTK_DOC_V_XREF)gtkdoc-fixxref --module=$(DOC_MODULE) --module-dir=html --html-dir=$(HTML_DIR) 
$(FIXXREF_OPTIONS)
+       $(AM_V_at)touch html-build.stamp
+
+#### pdf ####
+
+GTK_DOC_V_PDF=$(GTK_DOC_V_PDF_$(V))
+GTK_DOC_V_PDF_0=@echo "  DOC   Building PDF";
+GTK_DOC_V_PDF_1=
+
+pdf-build.stamp: sgml.stamp $(DOC_MAIN_SGML_FILE) $(content_files) $(expand_content_files)
+       $(GTK_DOC_V_PDF)rm -f $(DOC_MODULE).pdf && \
+       mkpdf_options=""; \
+       gtkdoc-mkpdf 2>&1 --help | grep  >/dev/null "\-\-verbose"; \
+       if test "$$?" = "0"; then \
+         if test "x$(V)" = "x1"; then \
+           mkpdf_options="$$mkpdf_options --verbose"; \
+         fi; \
+       fi; \
+       if test "x$(HTML_IMAGES)" != "x"; then \
+         for img in $(HTML_IMAGES); do \
+           part=`dirname $$img`; \
+           echo $$mkpdf_options | grep >/dev/null "\-\-imgdir=$$part "; \
+           if test $$? != 0; then \
+             mkpdf_options="$$mkpdf_options --imgdir=$$part"; \
+           fi; \
+         done; \
+       fi; \
+       gtkdoc-mkpdf --path="$(abs_srcdir)" $$mkpdf_options $(DOC_MODULE) $(DOC_MAIN_SGML_FILE) 
$(MKPDF_OPTIONS)
+       $(AM_V_at)touch pdf-build.stamp
+
+##############
+
+clean-local:
+       @rm -f *~ *.bak
+       @if echo $(SCAN_OPTIONS) | grep -q "\-\-rebuild-types" ; then \
+         rm -f $(DOC_MODULE).types; \
+       fi
+       @if echo $(SCAN_OPTIONS) | grep -q "\-\-rebuild-sections" ; then \
+         rm -f $(DOC_MODULE)-sections.txt; \
+       fi
+
+distclean-local:
+       @rm -rf xml html $(REPORT_FILES) $(DOC_MODULE).pdf \
+           $(DOC_MODULE)-decl-list.txt $(DOC_MODULE)-decl.txt
+       @if test "$(abs_srcdir)" != "$(abs_builddir)" ; then \
+           rm -f $(SETUP_FILES) $(DOC_MODULE).types; \
+       fi
+
+maintainer-clean-local:
+       @rm -rf xml html
+
+install-data-local:
+       @installfiles=`echo $(builddir)/html/*`; \
+       if test "$$installfiles" = '$(builddir)/html/*'; \
+       then echo 1>&2 'Nothing to install' ; \
+       else \
+         if test -n "$(DOC_MODULE_VERSION)"; then \
+           installdir="$(DESTDIR)$(TARGET_DIR)-$(DOC_MODULE_VERSION)"; \
+         else \
+           installdir="$(DESTDIR)$(TARGET_DIR)"; \
+         fi; \
+         $(MKDIR_P) $${installdir} ; \
+         for i in $$installfiles; do \
+           echo ' $(INSTALL_DATA) '$$i ; \
+           $(INSTALL_DATA) $$i $${installdir}; \
+         done; \
+         if test -n "$(DOC_MODULE_VERSION)"; then \
+           mv -f $${installdir}/$(DOC_MODULE).devhelp2 \
+             $${installdir}/$(DOC_MODULE)-$(DOC_MODULE_VERSION).devhelp2; \
+         fi; \
+         $(GTKDOC_REBASE) --relative --dest-dir=$(DESTDIR) --html-dir=$${installdir}; \
+       fi
+
+uninstall-local:
+       @if test -n "$(DOC_MODULE_VERSION)"; then \
+         installdir="$(DESTDIR)$(TARGET_DIR)-$(DOC_MODULE_VERSION)"; \
+       else \
+         installdir="$(DESTDIR)$(TARGET_DIR)"; \
+       fi; \
+       rm -rf $${installdir}
+
+dist-check-gtkdoc: docs
+
+dist-hook: dist-check-gtkdoc all-gtk-doc dist-hook-local
+       @$(MKDIR_P) $(distdir)/html
+       @cp ./html/* $(distdir)/html
+       @-cp ./$(DOC_MODULE).pdf $(distdir)/
+       @-cp ./$(DOC_MODULE).types $(distdir)/
+       @-cp ./$(DOC_MODULE)-sections.txt $(distdir)/
+       @cd $(distdir) && rm -f $(DISTCLEANFILES)
+       @$(GTKDOC_REBASE) --online --relative --html-dir=$(distdir)/html
+
+.PHONY : dist-hook-local docs
+
+meson.stamp: docs
+       @touch meson.stamp
diff --git a/doc/reference/gtk3/meson.build b/doc/reference/gtk3/meson.build
new file mode 100644
index 00000000..f03b2580
--- /dev/null
+++ b/doc/reference/gtk3/meson.build
@@ -0,0 +1,55 @@
+# Copyright © 2020, 2021 Christian Persch
+#
+# This library is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library.  If not, see <https://www.gnu.org/licenses/>.
+
+make_args_gtk3 = [
+  '-f', meson.current_source_dir() / '..' / 'Makefile.docs',
+  '--directory', meson.current_build_dir(),
+  'srcdir=' + meson.current_source_dir(),
+  'builddir=' + meson.current_build_dir(),
+  'abs_top_srcdir=' + meson.source_root(),
+  'abs_top_builddir=' + meson.build_root(),
+  'datadir=' + vte_prefix / vte_datadir,
+  'CAIRO_PREFIX=' + cairo_dep.get_pkgconfig_variable('prefix'),
+  'GLIB_PREFIX=' + glib_dep.get_pkgconfig_variable('prefix'),
+  'GTK_PREFIX=' + gtk3_dep.get_pkgconfig_variable('prefix'),
+  'PANGO_PREFIX=' + pango_dep.get_pkgconfig_variable('prefix'),
+  'CC=' + ' '.join(cc.cmd_array()),
+  'VERSION=' + meson.project_version(),
+  'VTE_API_VERSION=' + vte_api_version,
+  'VTE_GTK=3',
+  'VTE_LIB=' + libvte_gtk3.full_path(),
+]
+
+stamp = custom_target(
+  'meson.stamp',
+  build_by_default: true,
+  capture: false,
+  command: [make] + make_args_gtk3 + [
+    'meson.stamp',
+  ],
+  depends: [
+    libvte_gtk3,
+  ],
+  install: false,
+  output: 'meson.stamp',
+)
+
+meson.add_install_script(
+  make,
+  make_args_gtk3,
+  'install-data-local',
+)
+
+# Unfortunately, there's no way to hook up the 'clean-local' target
diff --git a/doc/reference/gtk4/meson.build b/doc/reference/gtk4/meson.build
new file mode 100644
index 00000000..7f42e86f
--- /dev/null
+++ b/doc/reference/gtk4/meson.build
@@ -0,0 +1,55 @@
+# Copyright © 2020, 2021 Christian Persch
+#
+# This library is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library.  If not, see <https://www.gnu.org/licenses/>.
+
+make_args_gtk4 = [
+  '-f', meson.current_source_dir() / '..' / 'Makefile.docs',
+  '--directory', meson.current_build_dir(),
+  'srcdir=' + meson.current_source_dir(),
+  'builddir=' + meson.current_build_dir(),
+  'abs_top_srcdir=' + meson.source_root(),
+  'abs_top_builddir=' + meson.build_root(),
+  'datadir=' + vte_prefix / vte_datadir,
+  'CAIRO_PREFIX=' + cairo_dep.get_pkgconfig_variable('prefix'),
+  'GLIB_PREFIX=' + glib_dep.get_pkgconfig_variable('prefix'),
+  'GTK_PREFIX=' + gtk4_dep.get_pkgconfig_variable('prefix'),
+  'PANGO_PREFIX=' + pango_dep.get_pkgconfig_variable('prefix'),
+  'CC=' + ' '.join(cc.cmd_array()),
+  'VERSION=' + meson.project_version(),
+  'VTE_API_VERSION=' + vte_api_version,
+  'VTE_GTK=4',
+  'VTE_LIB=' + libvte_gtk4.full_path(),
+]
+
+stamp = custom_target(
+  'meson.stamp',
+  build_by_default: true,
+  capture: false,
+  command: [make] + make_args_gtk4 + [
+    'meson.stamp',
+  ],
+  depends: [
+    libvte_gtk4,
+  ],
+  install: false,
+  output: 'meson.stamp',
+)
+
+meson.add_install_script(
+  make,
+  make_args_gtk4,
+  'install-data-local',
+)
+
+# Unfortunately, there's no way to hook up the 'clean-local' target
diff --git a/doc/reference/meson.build b/doc/reference/meson.build
index b47bc7ed..793bed5d 100644
--- a/doc/reference/meson.build
+++ b/doc/reference/meson.build
@@ -1,4 +1,4 @@
-# Copyright © 2018, 2019 Iñigo Martínez
+# Copyright © 2021 Christian Persch
 #
 # This library is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Lesser General Public License as published
@@ -13,96 +13,18 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with this library.  If not, see <https://www.gnu.org/licenses/>.
 
-# Meson insufficiency! Would be so much easier to just make this
-# (vte_gtk3_sources - vte_gtk3_public_headers).filter('.h'), but
-# but there is no array subtraction or filtering. Or just allow listing the
-# headers that we DO want to get scanned.
-# So instead just list all the headers here again... :-(
+# It turned out to be impossible to build gtk3 and gtk4 versions of the
+# docs from the same source using meson's gnome.gtkdoc(). Instead, build
+# using make with a gtk-doc.make-derived Makefile.
 
-private_headers = [
-  'buffer.h',
-  'caps.hh',
-  'cell.hh',
-  'config.h',
-  'debug.h',
-  'keymap.h',
-  'marshal.h',
-  'modes.hh',
-  'modes-ecma.hh',
-  'modes-private.hh',
-  'parser.hh',
-  'parser-arg.hh',
-  'parser-c01.hh',
-  'parser-charset.hh',
-  'parser-charset-tables.hh',
-  'parser-cmd.hh',
-  'parser-csi.hh',
-  'parser-dcs.hh',
-  'parser-esc.hh',
-  'parser-glue.hh',
-  'parser-osc.hh',
-  'parser-reply.hh',
-  'parser-string.hh',
-  'ring.hh',
-  'tabstops.hh',
-  'vteconv.h',
-  'vtedraw.h',
-  'vteinternal.hh',
-  'vterowdata.hh',
-  'vtestream-base.h',
-  'vtestream-file.h',
-  'vtestream.h',
-  'vtetypebuiltins.h',
-  'vteunistr.h',
-]
+make = find_program('gmake', 'make')
 
-scan_args = [
-  '--deprecated-guards="VTE_DISABLE_DEPRECATED"',
-  '--ignore-decorators=_VTE_GNUC_NONNULL\s*\([^)]*\)|_VTE_CXX_NOEXCEPT',
-]
-
-glib_prefix = glib_dep.get_pkgconfig_variable('prefix')
-
-version_conf = configuration_data()
-version_conf.set('VERSION', vte_version)
-
-content_files = configure_file(
-  input: 'version.xml.in',
-  output: '@BASENAME@',
-  configuration: version_conf
-)
+cairo_dep = dependency('cairo')
 
 if get_option('gtk3')
-  gtk3_prefix = gtk3_dep.get_pkgconfig_variable('prefix')
-
-  fixxref_args = [
-    '--html-dir=' + (vte_prefix / gnome.gtkdoc_html_dir(vte_gtk3_api_name)),
-    '--extra-dir=' + (glib_prefix / gnome.gtkdoc_html_dir('glib')),
-    '--extra-dir=' + (glib_prefix / gnome.gtkdoc_html_dir('gio')),
-    '--extra-dir=' + (gtk3_prefix / gnome.gtkdoc_html_dir('gdk')),
-    '--extra-dir=' + (gtk3_prefix / gnome.gtkdoc_html_dir('gdk-pixbuf')),
-    '--extra-dir=' + (gtk3_prefix / gnome.gtkdoc_html_dir('gtk')),
-  ]
-
-  gnome.gtkdoc(
-    'vte',
-    main_xml: 'vte-docs.xml',
-    module_version: vte_api_version,
-    src_dir: [src_inc, vte_inc],
-    ignore_headers: private_headers,
-    include_directories: top_inc,
-    dependencies: libvte_gtk3_dep,
-    c_args: '-DVTE_COMPILATION',
-    namespace: 'vte',
-    scan_args: scan_args,
-    mkdb_args: '--source-suffixes=h,hh,c,cc',
-    fixxref_args: fixxref_args,
-    gobject_typesfile: 'vte.types',
-    content_files: content_files,
-    install: true,
-  )
+  subdir('gtk3')
 endif
 
 if get_option('gtk4')
-  assert(false, 'not yet supported')
+  subdir('gtk4')
 endif
diff --git a/doc/reference/vte-docs.xml b/doc/reference/vte-docs.xml
index e3cd5180..0479d58c 100644
--- a/doc/reference/vte-docs.xml
+++ b/doc/reference/vte-docs.xml
@@ -1,7 +1,9 @@
 <?xml version="1.0"?>
 <!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.1.2//EN"
-               "http://www.oasis-open.org/docbook/xml/4.1.2/docbookx.dtd"; [
-<!ENTITY version SYSTEM "version.xml">
+               "http://www.oasis-open.org/docbook/xml/4.1.2/docbookx.dtd";
+[
+  <!ENTITY % gtkdocentities SYSTEM "xml/gtkdocentities.ent">
+  %gtkdocentities;
 ]>
 <book id="index" xmlns:xi="http://www.w3.org/2003/XInclude";>
   <!--
@@ -24,9 +26,9 @@
   <bookinfo>
     <title>VTE Reference Manual</title>
     <releaseinfo>
-      Documentation for VTE version &version;.
+      Documentation for VTE version &package_version;.
       The latest version of this documentation can be found on-line at the
-      <ulink role="online-location" url="http://library.gnome.org/devel/vte/";>GNOME Library</ulink>.
+      <ulink role="online-location" url="https://library.gnome.org/devel/vte/";>GNOME Library</ulink>.
     </releaseinfo>
 
     <copyright>
@@ -133,8 +135,12 @@
     <title>Index of new symbols in 0.64</title>
     <xi:include href="xml/api-index-0.64.xml"><xi:fallback /></xi:include>
   </index>
+  <index id="api-index-0-66" role="0.66">
+    <title>Index of new symbols in 0.66</title>
+    <xi:include href="xml/api-index-0.66.xml"><xi:fallback /></xi:include>
+  </index>
 
-  <xi:include href="xml/annotation-glossary.xml"><xi:fallback /></xi:include>
+  <xi:include href="xml/annotation-glossary.xml"></xi:include>
 
   <appendix id="licence">
     <title>Licence</title>
diff --git a/doc/reference/vte-overrides.txt b/doc/reference/vte-overrides.txt.in
similarity index 100%
rename from doc/reference/vte-overrides.txt
rename to doc/reference/vte-overrides.txt.in
diff --git a/doc/reference/vte-sections.txt b/doc/reference/vte-sections.txt.in
similarity index 96%
rename from doc/reference/vte-sections.txt
rename to doc/reference/vte-sections.txt.in
index c69181da..84afe8c3 100644
--- a/doc/reference/vte-sections.txt
+++ b/doc/reference/vte-sections.txt.in
@@ -71,12 +71,16 @@ vte_terminal_reset
 vte_terminal_get_text
 vte_terminal_get_text_range
 vte_terminal_get_cursor_position
+#if VTE_GTK == 3
 vte_terminal_hyperlink_check_event
+#endif
 vte_terminal_match_add_regex
 vte_terminal_match_remove
 vte_terminal_match_remove_all
 vte_terminal_match_check
+#if VTE_GTK == 3
 vte_terminal_match_check_event
+#endif
 vte_terminal_match_set_cursor_name
 vte_terminal_set_cjk_ambiguous_width
 vte_terminal_get_cjk_ambiguous_width
@@ -93,8 +97,10 @@ vte_terminal_search_get_regex
 vte_terminal_search_get_wrap_around
 vte_terminal_search_set_regex
 vte_terminal_search_set_wrap_around
+#if VTE_GTK == 3
 vte_terminal_event_check_regex_array
 vte_terminal_event_check_regex_simple
+#endif /* VTE_GTK */
 
 <SUBSECTION>
 VteFeatureFlags
@@ -113,9 +119,11 @@ vte_terminal_set_pty
 vte_terminal_pty_new_sync
 vte_terminal_watch_child
 
+#if VTE_GTK == 3
 <SUBSECTION>
 vte_terminal_set_clear_background
 vte_terminal_get_color_background_for_draw
+#endif /* VTE_GTK == 3 */
 
 <SUBSECTION Standard>
 VTE_TYPE_CURSOR_BLINK_MODE
@@ -150,14 +158,18 @@ vte_terminal_get_current_file_uri
 <SUBSECTION Deprecated>
 vte_terminal_copy_clipboard
 vte_terminal_match_set_cursor
+#if VTE_GTK == 3
 vte_terminal_match_set_cursor_type
 vte_terminal_match_add_gregex
 vte_terminal_search_get_gregex
 vte_terminal_search_set_gregex
 vte_terminal_event_check_gregex_simple
+#endif /* VTE_GTK == 3 */
 vte_terminal_spawn_sync
+#if VTE_GTK == 3
 vte_terminal_get_geometry_hints
 vte_terminal_set_geometry_hints_for_window
+#endif /* VTE_GTK == 3 */
 vte_terminal_get_icon_title
 vte_terminal_set_encoding
 vte_terminal_get_encoding
diff --git a/doc/reference/vte.types b/doc/reference/vte.types.in
similarity index 100%
rename from doc/reference/vte.types
rename to doc/reference/vte.types.in
diff --git a/meson.build b/meson.build
index 5245090a..070d43a4 100644
--- a/meson.build
+++ b/meson.build
@@ -34,7 +34,10 @@ project(
 gtk3_req_version          = '3.20.0'
 gtk3_min_req_version      = '3.18'
 gtk3_max_allowed_version  = '3.20'
-gtk4_req_version          = '4.0.0'
+
+gtk4_req_version          = '4.0.1'
+gtk4_min_req_version      = '4.0'
+gtk4_max_allowed_version  = '4.0'
 
 fribidi_req_version       = '1.0.0'
 gio_req_version           = '2.52.0'
@@ -54,11 +57,8 @@ vte_api_minor_version = 91
 vte_api_version = '@0@.@1@'.format(vte_api_major_version, vte_api_minor_version)
 vte_api_name = 'vte-@0@.@1@'.format(vte_api_major_version, vte_api_minor_version)
 
-vte_gtk3_api_version = '@0@.@1@'.format(vte_api_major_version, vte_api_minor_version)
-vte_gtk4_api_version = '@0@.@1@'.format(vte_api_major_version + 1, vte_api_minor_version)
-
-vte_gtk3_api_name = 'vte-' + vte_gtk3_api_version
-vte_gtk4_api_name = 'vte-' + vte_gtk4_api_version
+vte_gtk3_api_name = 'vte-' + vte_api_version
+vte_gtk4_api_name = 'vte-' + vte_api_version + '-gtk4'
 
 vte_gtk3_api_path = vte_gtk3_api_name / 'vte'
 vte_gtk4_api_path = vte_gtk4_api_name / 'vte'
@@ -141,6 +141,17 @@ if get_option('gtk3')
   gtk3_version_cppflags += '-DGDK_VERSION_MAX_ALLOWED=(G_ENCODE_VERSION(' + ver[0] + ',' + ver[1] + '))'
 endif
 
+if get_option('gtk4')
+  gtk4_version_cppflags = []
+
+  ver = gtk4_min_req_version.split('.')
+  gtk4_version_cppflags += '-DGDK_VERSION_MIN_REQUIRED=(G_ENCODE_VERSION(' + ver[0] + ',' + ver[1] + '))'
+
+  ver = gtk4_max_allowed_version.split('.')
+  gtk4_version_cppflags += '-DGDK_VERSION_MAX_ALLOWED=(G_ENCODE_VERSION(' + ver[0] + ',' + ver[1] + '))'
+endif
+
+
 # FIXME AC_USE_SYSTEM_EXTENSIONS also supported non-gnu systems
 config_h.set10('_GNU_SOURCE', true)
 
@@ -427,7 +438,7 @@ else
 endif
 
 if get_option('gtk4')
-  gtk4_dep = dependency('gtk+-4.0', version: '>=' + gtk4_req_version)
+  gtk4_dep = dependency('gtk4', version: '>=' + gtk4_req_version)
 else
   gtk4_dep = dependency('', required: false)
 endif
@@ -462,6 +473,10 @@ subdir('bindings')
 subdir('po')
 
 if get_option('docs')
+  assert(meson.version().version_compare('>= 0.55.0'),
+    'meson >= 0.55 is required to build docs'
+  )
+
   subdir('doc/reference')
 endif
 
@@ -470,8 +485,7 @@ endif
 makefile_conf = configuration_data()
 makefile_conf.set('srcdir', meson.current_source_dir())
 makefile_conf.set('builddir', meson.current_build_dir())
-makefile_conf.set('vte_gtk3_api_version', vte_gtk3_api_version)
-makefile_conf.set('vte_gtk4_api_version', vte_gtk4_api_version)
+makefile_conf.set('vte_api_version', vte_api_version)
 
 configure_file(
   input: 'Makefile.meson',
@@ -516,4 +530,8 @@ output += '\n'
 output += '  Prefix:       ' + get_option('prefix') + '\n'
 message(output)
 
+if get_option('gtk4')
+  warning('GTK+ 4.0 support is experimental; API and ABI are subject to change without notice\n')
+endif
+
 # Done
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index fffa7710..13d510de 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -3,8 +3,8 @@ bindings/vala/search-popover.ui
 src/vtespawn.cc
 src/app/app.cc
 src/app/appmenu-gtk3.ui
-src/app/appmenu.ui
+src/app/appmenu-gtk4.ui
 src/app/search-popover-gtk3.ui
-src/app/search-popover.ui
+src/app/search-popover-gtk4.ui
 src/app/window-gtk3.ui
-src/app/window.ui
+src/app/window-gtk4.ui
diff --git a/src/app/app-gtk4.gresource.xml b/src/app/app-gtk4.gresource.xml
new file mode 100644
index 00000000..6c76b21f
--- /dev/null
+++ b/src/app/app-gtk4.gresource.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright © 2014, 2020 Christian Persch
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <https://www.gnu.org/licenses/>.
+-->
+<gresources>
+  <gresource prefix="/org/gnome/vte/app">
+    <file alias="ui/search-popover.ui" compressed="true" 
preprocess="xml-stripblanks">search-popover-gtk4.ui</file>
+    <file alias="ui/window.ui" compressed="true" preprocess="xml-stripblanks">window-gtk4.ui</file>
+    <file alias="gtk/menus.ui" compressed="true" preprocess="xml-stripblanks">appmenu-gtk4.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/app/app.cc b/src/app/app.cc
index b761e17d..e7a20c87 100644
--- a/src/app/app.cc
+++ b/src/app/app.cc
@@ -38,7 +38,9 @@
 #include <vector>
 
 #include "std-glue.hh"
+#include "cairo-glue.hh"
 #include "glib-glue.hh"
+#include "gtk-glue.hh"
 #include "libc-glue.hh"
 #include "pango-glue.hh"
 #include "pcre2-glue.hh"
@@ -61,13 +63,11 @@ public:
         gboolean feed_stdin{false};
         gboolean icon_title{false};
         gboolean keep{false};
-        gboolean no_argb_visual{false};
         gboolean no_bidi{false};
         gboolean no_bold{false};
         gboolean no_builtin_dingus{false};
         gboolean no_context_menu{false};
         gboolean no_decorations{false};
-        gboolean no_double_buffer{false};
         gboolean no_fallback_scrolling{false};
         gboolean no_geometry_hints{false};
         gboolean no_hyperlink{false};
@@ -122,6 +122,11 @@ public:
         VteTextBlinkMode text_blink_mode{VTE_TEXT_BLINK_ALWAYS};
         vte::glib::RefPtr<GtkCssProvider> css{};
 
+#if VTE_GTK == 3
+        gboolean no_argb_visual{false};
+        gboolean no_double_buffer{false};
+#endif /* VTE_GTK == 3 */
+
         ~Options() {
                 g_clear_object(&background_pixbuf);
                 g_free(command);
@@ -366,15 +371,43 @@ private:
                 return that->parse_color(value, &that->bg_color, &set, error);
         }
 
+#if VTE_GTK == 4
+        static void
+        parse_css_error_cb(GtkCssProvider* provider,
+                           void* section,
+                           GError* error,
+                           GError** ret_error) noexcept
+        {
+                if (error)
+                        *ret_error = g_error_copy(error);
+        }
+#endif /* VTE_GTK == 4 */
+
         static gboolean
         parse_css_file(char const* option, char const* value, void* data, GError** error)
         {
                 Options* that = static_cast<Options*>(data);
 
                 auto css = vte::glib::take_ref(gtk_css_provider_new());
+#if VTE_GTK == 3
                 if (!gtk_css_provider_load_from_path(css.get(), value, error))
                     return false;
 
+#elif VTE_GTK == 4
+                GError* err = nullptr;
+                auto const id = g_signal_connect(css.get(), "parsing-error",
+                                                 G_CALLBACK(parse_css_error_cb), &err);
+
+                gtk_css_provider_load_from_path(css.get(), value);
+                g_signal_handler_disconnect(css.get(), id);
+                if (err) {
+                        g_propagate_prefixed_error(error, err,
+                                                   "Error parsing CSS file \"%s\": ",
+                                                   value);
+                        return false;
+                }
+#endif /* VTE_GTK */
+
                 that->css = std::move(css);
                 return true;
         }
@@ -551,8 +584,6 @@ public:
                           "Enable the setting of the icon title", nullptr },
                         { "keep", 'k', 0, G_OPTION_ARG_NONE, &keep,
                           "Live on after the command exits", nullptr },
-                        { "no-argb-visual", 0, 0, G_OPTION_ARG_NONE, &no_argb_visual,
-                          "Don't use an ARGB visual", nullptr },
                         { "no-bidi", 0, 0, G_OPTION_ARG_NONE, &no_bidi,
                           "Disable BiDi", nullptr },
                         { "no-bold", 0, 0, G_OPTION_ARG_NONE, &no_bold,
@@ -563,8 +594,6 @@ public:
                           "Disable context menu", nullptr },
                         { "no-decorations", 0, 0, G_OPTION_ARG_NONE, &no_decorations,
                           "Disable window decorations", nullptr },
-                        { "no-double-buffer", '2', 0, G_OPTION_ARG_NONE, &no_double_buffer,
-                          "Disable double-buffering", nullptr },
                         { "no-fallback-scrolling", 0, 0, G_OPTION_ARG_NONE, &no_fallback_scrolling,
                           "Disable fallback scrolling", nullptr },
                         { "no-geometry-hints", 'G', 0, G_OPTION_ARG_NONE, &no_geometry_hints,
@@ -620,8 +649,6 @@ public:
                           nullptr, nullptr },
                         { "console", 'C', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &console,
                           nullptr, nullptr },
-                        { "double-buffer", '2', G_OPTION_FLAG_REVERSE | G_OPTION_FLAG_HIDDEN,
-                          G_OPTION_ARG_NONE, &no_double_buffer, nullptr, nullptr },
                         { "pty-flags", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &dummy_string,
                           nullptr, nullptr },
                         { "scrollbar-policy", 'P', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING,
@@ -636,6 +663,15 @@ public:
 #endif
                         { "use-theme-colors", 0, 0, G_OPTION_ARG_NONE, &use_theme_colors,
                           "Use foreground and background colors from the gtk+ theme", nullptr },
+
+#if VTE_GTK == 3
+                        { "no-argb-visual", 0, 0, G_OPTION_ARG_NONE, &no_argb_visual,
+                          "Don't use an ARGB visual", nullptr },
+                        { "double-buffer", '2', G_OPTION_FLAG_REVERSE | G_OPTION_FLAG_HIDDEN,
+                          G_OPTION_ARG_NONE, &no_double_buffer, nullptr, nullptr },
+                        { "no-double-buffer", '2', 0, G_OPTION_ARG_NONE, &no_double_buffer,
+                          "Disable double-buffering", nullptr },
+#endif /* VTE_GTK == 3 */
                         { nullptr }
                 };
 
@@ -669,7 +705,9 @@ public:
                 g_option_group_add_entries(group, entries);
                 g_option_context_set_main_group(context.get(), group);
 
+#if VTE_GTK == 3
                 g_option_context_add_group(context.get(), gtk_get_option_group(true));
+#endif
 
                 bool rv = g_option_context_parse(context.get(), &argc, &argv, error);
 
@@ -680,6 +718,14 @@ public:
                         swap(fg_color, bg_color);
                 }
 
+#if VTE_GTK == 4
+                if (rv && !gtk_init_check()) {
+                        g_set_error_literal(error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED,
+                                            "Failed to initialise gtk+");
+                        rv = false;
+                }
+#endif /* VTE_GTK == 4 */
+
                 return rv;
         }
 };
@@ -811,7 +857,11 @@ vteapp_search_popover_update_sensitivity(VteappSearchPopover* popover)
 static void
 vteapp_search_popover_update_regex(VteappSearchPopover* popover)
 {
-        char const* search_text = gtk_entry_get_text(GTK_ENTRY(popover->search_entry));
+#if VTE_GTK == 3
+        auto search_text = gtk_entry_get_text(GTK_ENTRY(popover->search_entry));
+#elif VTE_GTK == 4
+        auto search_text = gtk_editable_get_text(GTK_EDITABLE(popover->search_entry));
+#endif /* VTE_GTK */
         bool caseless = gtk_toggle_button_get_active(popover->match_case_checkbutton) == FALSE;
 
         char* pattern;
@@ -974,10 +1024,18 @@ static GtkWidget*
 vteapp_search_popover_new(VteTerminal* terminal,
                           GtkWidget* relative_to)
 {
-        return reinterpret_cast<GtkWidget*>(g_object_new(VTEAPP_TYPE_SEARCH_POPOVER,
-                                                         "terminal", terminal,
-                                                         "relative-to", relative_to,
-                                                         nullptr));
+        auto popover = reinterpret_cast<GtkWidget*>(g_object_new(VTEAPP_TYPE_SEARCH_POPOVER,
+                                                                 "terminal", terminal,
+#if VTE_GTK == 3
+                                                                 "relative-to", relative_to,
+#endif
+                                                                 nullptr));
+
+#if VTE_GTK == 4
+        gtk_widget_set_parent(popover, relative_to);
+#endif
+
+        return popover;
 }
 
 /* terminal */
@@ -1015,35 +1073,53 @@ vteapp_terminal_realize(GtkWidget* widget)
 {
         GTK_WIDGET_CLASS(vteapp_terminal_parent_class)->realize(widget);
 
-        VteappTerminal* terminal = VTEAPP_TERMINAL(widget);
-        if (options.background_pixbuf != nullptr) {
-                auto surface = gdk_cairo_surface_create_from_pixbuf(options.background_pixbuf,
-                                                                    0 /* take scale from window */,
-                                                                    gtk_widget_get_window(widget));
-                terminal->background_pattern = cairo_pattern_create_for_surface(surface);
-                cairo_surface_destroy(surface);
+        if (!options.background_pixbuf)
+                return;
 
-                cairo_pattern_set_extend(terminal->background_pattern, options.background_extend);
-        }
+        auto terminal = VTEAPP_TERMINAL(widget);
+
+#if VTE_GTK == 3
+        auto surface = vte::take_freeable
+                (gdk_cairo_surface_create_from_pixbuf(options.background_pixbuf,
+                                                      0 /* take scale from window */,
+                                                      gtk_widget_get_window(widget)));
+#elif VTE_GTK == 4
+        auto const width = gdk_pixbuf_get_width(options.background_pixbuf);
+        auto const height = gdk_pixbuf_get_height(options.background_pixbuf);
+        auto surface = vte::take_freeable(cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
+                                                                     width, height));
+        auto cr = vte::take_freeable(cairo_create(surface.get()));
+        gdk_cairo_set_source_pixbuf(cr.get(), options.background_pixbuf, 0, 0);
+        cairo_paint(cr.get());
+        cairo_surface_flush(surface.get()); // FIXME necessary?
+#endif
+        terminal->background_pattern = cairo_pattern_create_for_surface(surface.get());
+
+        cairo_pattern_set_extend(terminal->background_pattern, options.background_extend);
 }
 
 static void
 vteapp_terminal_unrealize(GtkWidget* widget)
 {
-        VteappTerminal* terminal = VTEAPP_TERMINAL(widget);
+#if VTE_GTK == 3
+        auto terminal = VTEAPP_TERMINAL(widget);
+
         if (terminal->background_pattern != nullptr) {
                 cairo_pattern_destroy(terminal->background_pattern);
                 terminal->background_pattern = nullptr;
         }
+#endif /* VTE_GTK */
 
         GTK_WIDGET_CLASS(vteapp_terminal_parent_class)->unrealize(widget);
 }
 
-static gboolean
-vteapp_terminal_draw(GtkWidget* widget,
-                     cairo_t* cr)
+static void
+vteapp_terminal_draw_background(GtkWidget* widget,
+                                cairo_t* cr)
 {
-        VteappTerminal* terminal = VTEAPP_TERMINAL(widget);
+#if VTE_GTK == 3
+        auto terminal = VTEAPP_TERMINAL(widget);
+
         if (terminal->background_pattern != nullptr) {
                 cairo_push_group(cr);
 
@@ -1066,8 +1142,29 @@ vteapp_terminal_draw(GtkWidget* widget,
                 cairo_paint_with_alpha(cr, options.get_alpha_bg_for_draw());
 
         }
+#endif /* VTE_GTK == 3 */
+}
 
-        auto rv = GTK_WIDGET_CLASS(vteapp_terminal_parent_class)->draw(widget, cr);
+#if VTE_GTK == 4
+
+static void
+vteapp_terminal_draw_background(GtkWidget* widget,
+                                GtkSnapshot* snapshot)
+{
+        auto grect = GRAPHENE_RECT_INIT(float(0), float(0),
+                                        float(gtk_widget_get_allocated_width(widget)),
+                                        float(gtk_widget_get_allocated_height(widget)));
+        auto cr = vte::take_freeable(gtk_snapshot_append_cairo(snapshot, &grect));
+        vteapp_terminal_draw_background(widget, cr.get());
+}
+
+#endif /* VTE_GTK  == 4 */
+
+static void
+vteapp_terminal_draw_backdrop(GtkWidget* widget,
+                              cairo_t* cr)
+{
+        auto terminal = VTEAPP_TERMINAL(widget);
 
         if (terminal->use_backdrop && terminal->has_backdrop) {
                 cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
@@ -1077,49 +1174,162 @@ vteapp_terminal_draw(GtkWidget* widget,
                                 gtk_widget_get_allocated_height(widget));
                 cairo_paint(cr);
         }
+}
+
+#if VTE_GTK == 4
+
+static void
+vteapp_terminal_draw_backdrop(GtkWidget* widget,
+                              GtkSnapshot* snapshot)
+{
+        auto grect = GRAPHENE_RECT_INIT(float(0), float(0),
+                                        float(gtk_widget_get_allocated_width(widget)),
+                                        float(gtk_widget_get_allocated_height(widget)));
+        auto cr = vte::take_freeable(gtk_snapshot_append_cairo(snapshot, &grect));
+        vteapp_terminal_draw_backdrop(widget, cr.get());
+}
+
+#endif /* VTE_GTK  == 4 */
+
+#if VTE_GTK == 3
+
+static gboolean
+vteapp_terminal_draw(GtkWidget* widget,
+                     cairo_t* cr)
+{
+        vteapp_terminal_draw_background(widget, cr);
+
+        auto const rv = GTK_WIDGET_CLASS(vteapp_terminal_parent_class)->draw(widget, cr);
+
+        vteapp_terminal_draw_backdrop(widget, cr);
 
         return rv;
 }
 
-static auto dti(double d) -> unsigned { return CLAMP((d*255), 0, 255); }
+#endif /* VTE_GTK == 3 */
+
+static void
+vteapp_terminal_update_theme_colors(GtkWidget* widget)
+{
+        if (!options.use_theme_colors)
+                return;
+
+        auto terminal = VTEAPP_TERMINAL(widget);
+        auto context = gtk_widget_get_style_context(widget);
+
+#if VTE_GTK == 3
+        auto const flags = gtk_style_context_get_state(context);
+#endif
+
+        auto theme_fg = GdkRGBA{};
+        gtk_style_context_get_color(context,
+#if VTE_GTK == 3
+                                    flags,
+#endif
+                                    &theme_fg);
+
+        auto theme_bg = GdkRGBA{};
+#if VTE_GTK == 3
+        G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
+        gtk_style_context_get_background_color(context, flags, &theme_bg);
+        G_GNUC_END_IGNORE_DEPRECATIONS;
+#elif VTE_GTK == 4
+        // FIXMEgtk4 "background-color" lookup always fails
+        if (!gtk_style_context_lookup_color(context, "text_view_bg", &theme_bg)) {
+                verbose_print("Failed to get theme background color\n");
+                return;
+        }
+#endif
+
+        auto dti = [](double d) -> unsigned { return std::clamp(unsigned(d*255), 0u, 255u); };
+
+        verbose_print("Theme colors: foreground is #%02X%02X%02X, background is #%02X%02X%02X\n",
+                      dti(theme_fg.red), dti(theme_fg.green), dti(theme_fg.blue),
+                      dti(theme_bg.red), dti(theme_bg.green), dti(theme_bg.blue));
+
+        theme_fg.alpha = 1.;
+        theme_bg.alpha = options.get_alpha_bg();
+        vte_terminal_set_colors(VTE_TERMINAL(terminal), &theme_fg, &theme_bg, nullptr, 0);
+}
+
+#if VTE_GTK == 3
 
 static void
 vteapp_terminal_style_updated(GtkWidget* widget)
 {
         GTK_WIDGET_CLASS(vteapp_terminal_parent_class)->style_updated(widget);
 
-        auto context = gtk_widget_get_style_context(widget);
-        auto flags = gtk_style_context_get_state(context);
+        auto terminal = VTEAPP_TERMINAL(widget);
 
-        VteappTerminal* terminal = VTEAPP_TERMINAL(widget);
+        auto context = gtk_widget_get_style_context(widget);
+        auto const flags = gtk_style_context_get_state(context);
         terminal->has_backdrop = (flags & GTK_STATE_FLAG_BACKDROP) != 0;
 
-        if (options.use_theme_colors) {
-                auto theme_fg = GdkRGBA{};
-                gtk_style_context_get_color(context, flags, &theme_fg);
-                G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
-                auto theme_bg = GdkRGBA{};
-                gtk_style_context_get_background_color(context, flags, &theme_bg);
-                G_GNUC_END_IGNORE_DEPRECATIONS;
+        vteapp_terminal_update_theme_colors(widget);
+}
 
-                verbose_print("Theme colors: foreground is #%02X%02X%02X, background is #%02X%02X%02X\n",
-                              dti(theme_fg.red), dti(theme_fg.green), dti(theme_fg.blue),
-                              dti(theme_bg.red), dti(theme_bg.green), dti(theme_bg.blue));
+#endif /* VTE_GTK == 3 */
 
-                theme_fg.alpha = 1.;
-                theme_bg.alpha = options.get_alpha_bg();
-                vte_terminal_set_colors(VTE_TERMINAL(terminal), &theme_fg, &theme_bg, nullptr, 0);
-        }
+#if VTE_GTK == 4
+
+static void
+vteapp_terminal_snapshot(GtkWidget* widget,
+                         GtkSnapshot* snapshot_object)
+{
+        vteapp_terminal_draw_background(widget, snapshot_object);
+
+        GTK_WIDGET_CLASS(vteapp_terminal_parent_class)->snapshot(widget, snapshot_object);
+
+        vteapp_terminal_draw_backdrop(widget, snapshot_object);
+}
+
+static void
+vteapp_terminal_css_changed(GtkWidget* widget,
+                            GtkCssStyleChange* change)
+{
+        GTK_WIDGET_CLASS(vteapp_terminal_parent_class)->css_changed(widget, change);
+
+        vteapp_terminal_update_theme_colors(widget);
+}
+
+static void
+vteapp_terminal_state_flags_changed(GtkWidget* widget,
+                                    GtkStateFlags old_flags)
+{
+        GTK_WIDGET_CLASS(vteapp_terminal_parent_class)->state_flags_changed(widget, old_flags);
+
+        auto terminal = VTEAPP_TERMINAL(widget);
+        auto const flags = gtk_widget_get_state_flags(widget);
+        terminal->has_backdrop = (flags & GTK_STATE_FLAG_BACKDROP) != 0;
+}
+
+static void
+vteapp_terminal_system_setting_changed(GtkWidget* widget,
+                                       GtkSystemSetting setting)
+{
+        GTK_WIDGET_CLASS(vteapp_terminal_parent_class)->system_setting_changed(widget, setting);
+
+        // FIXMEgtk4 find a way to update colours on theme change like gtk3 above
 }
 
+#endif /* VTE_GTK == 4 */
+
 static void
 vteapp_terminal_class_init(VteappTerminalClass *klass)
 {
-        GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+        auto widget_class = GTK_WIDGET_CLASS(klass);
         widget_class->realize = vteapp_terminal_realize;
         widget_class->unrealize = vteapp_terminal_unrealize;
+
+#if VTE_GTK == 3
         widget_class->draw = vteapp_terminal_draw;
         widget_class->style_updated = vteapp_terminal_style_updated;
+#elif VTE_GTK == 4
+        widget_class->snapshot = vteapp_terminal_snapshot;
+        widget_class->css_changed = vteapp_terminal_css_changed;
+        widget_class->state_flags_changed = vteapp_terminal_state_flags_changed;
+        widget_class->system_setting_changed = vteapp_terminal_system_setting_changed;
+#endif
 
         gtk_widget_class_set_css_name(widget_class, "vteapp-terminal");
 }
@@ -1131,8 +1341,10 @@ vteapp_terminal_init(VteappTerminal *terminal)
         terminal->has_backdrop = false;
         terminal->use_backdrop = options.backdrop;
 
+#if VTE_GTK == 3
         if (options.background_pixbuf != nullptr)
                 vte_terminal_set_clear_background(VTE_TERMINAL(terminal), false);
+#endif /* VTE_GTK == 3 */
 }
 
 static GtkWidget *
@@ -1168,12 +1380,9 @@ struct _VteappWindow {
         /* end */
 
         VteTerminal* terminal;
-        GtkClipboard* clipboard;
         GPid child_pid;
         GtkWidget* search_popover;
 
-        bool fullscreen{false};
-
         /* used for updating the geometry hints */
         int cached_cell_width{0};
         int cached_cell_height{0};
@@ -1181,6 +1390,15 @@ struct _VteappWindow {
         int cached_chrome_height{0};
         int cached_csd_width{0};
         int cached_csd_height{0};
+
+#if VTE_GTK == 3
+        GtkClipboard* clipboard;
+        GdkWindowState window_state{GdkWindowState(0)};
+#endif
+#if VTE_GTK == 4
+        GdkClipboard* clipboard;
+        GdkToplevelState toplevel_state{GdkToplevelState(0)};
+#endif
 };
 
 struct _VteappWindowClass {
@@ -1224,6 +1442,7 @@ vteapp_window_add_dingus(VteappWindow* window,
 static void
 vteapp_window_update_geometry(VteappWindow* window)
 {
+#if VTE_GTK == 3
         GtkWidget* window_widget = GTK_WIDGET(window);
         GtkWidget* terminal_widget = GTK_WIDGET(window->terminal);
 
@@ -1304,18 +1523,43 @@ vteapp_window_update_geometry(VteappWindow* window)
                       window->cached_cell_width, window->cached_cell_height,
                       window->cached_chrome_width, window->cached_chrome_height,
                       window->cached_csd_width, window->cached_csd_height);
+#elif VTE_GTK == 4
+        // FIXMEgtk4 there appears to be no way to do this with gtk4 ? maybe go to X/wayland
+        // directly to set the geometry hints?
+#endif
 }
 
+#include <gdk/gdk.h>
+
 static void
 vteapp_window_resize(VteappWindow* window)
 {
-        /* Don't do this for maximised or tiled windows. */
-        auto win = gtk_widget_get_window(GTK_WIDGET(window));
-        if (win != nullptr &&
-            (gdk_window_get_state(win) & (GDK_WINDOW_STATE_MAXIMIZED |
-                                          GDK_WINDOW_STATE_FULLSCREEN |
-                                          GDK_WINDOW_STATE_TILED)) != 0)
+        /* Don't do this for fullscreened, maximised, or tiled windows. */
+#if VTE_GTK == 3
+        if (window->window_state & (GDK_WINDOW_STATE_MAXIMIZED |
+                                    GDK_WINDOW_STATE_FULLSCREEN |
+                                    GDK_WINDOW_STATE_TILED |
+#if GTK_CHECK_VERSION(3,22,23)
+                                    GDK_WINDOW_STATE_TOP_TILED |
+                                    GDK_WINDOW_STATE_BOTTOM_TILED |
+                                    GDK_WINDOW_STATE_LEFT_TILED |
+                                    GDK_WINDOW_STATE_RIGHT_TILED |
+#endif
+                                    0))
                 return;
+#elif VTE_GTK == 4
+        if (window->toplevel_state & (GDK_TOPLEVEL_STATE_MAXIMIZED |
+                                      GDK_TOPLEVEL_STATE_FULLSCREEN |
+                                      GDK_TOPLEVEL_STATE_TILED |
+                                      GDK_TOPLEVEL_STATE_TOP_TILED |
+                                      GDK_TOPLEVEL_STATE_BOTTOM_TILED |
+                                      GDK_TOPLEVEL_STATE_LEFT_TILED |
+                                      GDK_TOPLEVEL_STATE_RIGHT_TILED))
+                return;
+#endif /* VTE_GTK */
+
+#if VTE_GTK == 3
+        // FIXMEgtk4
 
         /* First, update the geometry hints, so that the cached_* members are up-to-date */
         vteapp_window_update_geometry(window);
@@ -1330,11 +1574,13 @@ vteapp_window_resize(VteappWindow* window)
                       columns, rows, pixel_width, pixel_height);
 
         gtk_window_resize(GTK_WINDOW(window), pixel_width, pixel_height);
+#endif /* VTE_GTK == 3 FIXMEgtk4 */
 }
 
 static void
 vteapp_window_parse_geometry(VteappWindow* window)
 {
+#if VTE_GTK == 3
         /* First update the geometry hints, so that gtk_window_parse_geometry()
          * knows the char width/height and base size increments.
          */
@@ -1379,6 +1625,9 @@ vteapp_window_parse_geometry(VteappWindow* window)
                         vteapp_window_resize(window);
                 }
         }
+#elif VTE_GTK == 4
+        // FIXMEgtk4 ????
+#endif /* VTE_GTK */
 }
 
 static void
@@ -1412,8 +1661,13 @@ window_spawn_cb(VteTerminal* terminal,
                 auto msg = vte::glib::take_string(g_strdup_printf("Spawning failed: %s", error->message));
                 if (options.keep)
                         vte_terminal_feed(window->terminal, msg.get(), -1);
-                else
+                else {
+#if VTE_GTK == 3
                         gtk_widget_destroy(GTK_WIDGET(window));
+#elif VTE_GTK == 4
+                        gtk_window_destroy(GTK_WINDOW(window));
+#endif
+                }
         }
 }
 
@@ -1570,17 +1824,35 @@ window_update_copy_sensitivity(VteappWindow* window)
                                     vte_terminal_get_has_selection(window->terminal));
 }
 
+static void
+window_update_fullscreen_state(VteappWindow* window)
+{
+#if VTE_GTK == 3
+        auto const fullscreen = (window->window_state & GDK_WINDOW_STATE_FULLSCREEN) != 0;
+#elif VTE_GTK == 4
+        auto const fullscreen = (window->toplevel_state & GDK_TOPLEVEL_STATE_FULLSCREEN) != 0;
+#endif
+        auto action = g_action_map_lookup_action(G_ACTION_MAP(window), "fullscreen");
+        g_simple_action_set_state(G_SIMPLE_ACTION(action), g_variant_new_boolean (fullscreen));
+}
+
 static void
 window_update_paste_sensitivity(VteappWindow* window)
 {
+        bool can_paste = false;
+
+#if VTE_GTK == 3
         GdkAtom* targets;
         int n_targets;
 
-        bool can_paste = false;
         if (gtk_clipboard_wait_for_targets(window->clipboard, &targets, &n_targets)) {
                 can_paste = gtk_targets_include_text(targets, n_targets);
                 g_free(targets);
         }
+#elif VTE_GTK == 4
+        auto formats = gdk_clipboard_get_formats(window->clipboard);
+        can_paste = gdk_content_formats_contain_gtype(formats, G_TYPE_STRING);
+#endif /* VTE_GTK */
 
         auto action = g_action_map_lookup_action(G_ACTION_MAP(window), "paste");
         g_simple_action_set_enabled(G_SIMPLE_ACTION(action), can_paste);
@@ -1606,10 +1878,15 @@ window_action_copy_match_cb(GSimpleAction* action,
                             GVariant* parameter,
                             void* data)
 {
-        VteappWindow* window = VTEAPP_WINDOW(data);
-        gsize len;
-        char const* str = g_variant_get_string(parameter, &len);
+        auto window = VTEAPP_WINDOW(data);
+
+        auto len = size_t{};
+        auto str = g_variant_get_string(parameter, &len);
+#if VTE_GTK == 3
         gtk_clipboard_set_text(window->clipboard, str, len);
+#elif VTE_GTK == 4
+        gdk_clipboard_set_text(window->clipboard, str);
+#endif
 }
 
 static void
@@ -1626,16 +1903,23 @@ window_action_reset_cb(GSimpleAction* action,
                        GVariant* parameter,
                        void* data)
 {
-        VteappWindow* window = VTEAPP_WINDOW(data);
-        bool clear;
-        GdkModifierType modifiers;
+        auto window = VTEAPP_WINDOW(data);
+        auto clear = false;
 
         if (parameter != nullptr)
                 clear = g_variant_get_boolean(parameter);
-        else if (gtk_get_current_event_state(&modifiers))
+        else {
+                auto modifiers = GdkModifierType{};
+#if VTE_GTK == 3
+                if (!gtk_get_current_event_state(&modifiers))
+                        modifiers = GdkModifierType(0);
+#elif VTE_GTK == 4
+                // FIXMEgtk4!
+                modifiers = GdkModifierType(0);
+#endif
+
                 clear = (modifiers & GDK_CONTROL_MASK) != 0;
-        else
-                clear = false;
+        }
 
         vte_terminal_reset(window->terminal, true, clear);
 }
@@ -1649,7 +1933,6 @@ window_action_find_cb(GSimpleAction* action,
         gtk_toggle_button_set_active(window->find_button, true);
 }
 
-
 static void
 window_action_fullscreen_state_cb (GSimpleAction *action,
                                    GVariant *state,
@@ -1668,6 +1951,7 @@ window_action_fullscreen_state_cb (GSimpleAction *action,
         /* The window-state-changed callback will update the action's actual state */
 }
 
+#if VTE_GTK == 3
 static bool
 vteapp_window_show_context_menu(VteappWindow* window,
                                 guint button,
@@ -1681,20 +1965,21 @@ vteapp_window_show_context_menu(VteappWindow* window,
         g_menu_append(menu, "_Copy", "win.copy::text");
         g_menu_append(menu, "Copy As _HTML", "win.copy::html");
 
-        if (event != nullptr) {
-                auto hyperlink = vte_terminal_hyperlink_check_event(window->terminal, event);
-                if (hyperlink != nullptr) {
-                        verbose_print("Hyperlink: %s\n", hyperlink);
-                        auto target = g_variant_new_string(hyperlink); /* floating */
+        if (event != nullptr)
+        {
+                auto hyperlink = vte::glib::take_string(vte_terminal_hyperlink_check_event(window->terminal, 
event));
+                if (hyperlink) {
+                        verbose_print("Hyperlink: %s\n", hyperlink.get());
+                        auto target = g_variant_new_string(hyperlink.get()); /* floating */
                         auto item = vte::glib::take_ref(g_menu_item_new("Copy _Hyperlink", nullptr));
                         g_menu_item_set_action_and_target_value(item.get(), "win.copy-match", target);
                         g_menu_append_item(menu, item.get());
                 }
 
-                auto match = vte_terminal_match_check_event(window->terminal, event, nullptr);
-                if (match != nullptr) {
-                        verbose_print("Match: %s\n", match);
-                        auto target = g_variant_new_string(match); /* floating */
+                auto match = vte::glib::take_string(vte_terminal_match_check_event(window->terminal, event, 
nullptr));
+                if (match) {
+                        verbose_print("Match: %s\n", match.get());
+                        auto target = g_variant_new_string(match.get()); /* floating */
                         auto item = vte::glib::take_ref(g_menu_item_new("Copy _Match", nullptr));
                         g_menu_item_set_action_and_target_value(item.get(), "win.copy-match", target);
                         g_menu_append_item(menu, item.get());
@@ -1728,15 +2013,14 @@ vteapp_window_show_context_menu(VteappWindow* window,
                         else
                                 verbose_print("%s match: %s\n", extra_pattern, extra_match);
                 }
-                g_free(hyperlink);
-                g_free(match);
                 g_free(extra_match);
                 g_free(extra_subst);
         }
 
         g_menu_append(menu, "_Paste", "win.paste");
 
-        if (window->fullscreen)
+        auto const fullscreen = (window->window_state & GDK_WINDOW_STATE_FULLSCREEN) != 0;
+        if (fullscreen)
                 g_menu_append(menu, "_Fullscreen", "win.fullscreen");
 
         auto popup = gtk_menu_new_from_model(G_MENU_MODEL(menu));
@@ -1747,13 +2031,23 @@ vteapp_window_show_context_menu(VteappWindow* window,
 
         return true;
 }
+#endif /* VTE_GTK */
+
+#if VTE_GTK == 3
 
 static gboolean
 window_popup_menu_cb(GtkWidget* widget,
                      VteappWindow* window)
 {
-        return vteapp_window_show_context_menu(window, 0, gtk_get_current_event_time(), nullptr);
+        auto const timestamp = gtk_get_current_event_time();
+
+        return vteapp_window_show_context_menu(window, 0, timestamp , nullptr);
 }
+// FIXMEgtk4
+
+#endif /* VTE_GTK == 3 */
+
+#if VTE_GTK == 3
 
 static gboolean
 window_button_press_cb(GtkWidget* widget,
@@ -1767,6 +2061,8 @@ window_button_press_cb(GtkWidget* widget,
                                                reinterpret_cast<GdkEvent*>(event));
 }
 
+#endif /* VTE_GTK == 3 */
+
 static void
 window_cell_size_changed_cb(VteTerminal* term,
                             guint width,
@@ -1818,9 +2114,15 @@ window_child_exited_cb(VteTerminal* term,
         if (options.keep)
                 return;
 
+#if VTE_GTK == 3
         gtk_widget_destroy(GTK_WIDGET(window));
+#elif VTE_GTK == 4
+        gtk_window_destroy(GTK_WINDOW(window));
+#endif
 }
 
+#if VTE_GTK == 3
+
 static void
 window_clipboard_owner_change_cb(GtkClipboard* clipboard,
                                  GdkEvent* event,
@@ -1829,6 +2131,18 @@ window_clipboard_owner_change_cb(GtkClipboard* clipboard,
         window_update_paste_sensitivity(window);
 }
 
+#elif VTE_GTK == 4
+
+static void
+window_clipboard_formats_notify_cb(GdkClipboard* clipboard,
+                                   GParamSpec* pspec,
+                                   VteappWindow* window)
+{
+        window_update_paste_sensitivity(window);
+}
+
+#endif /* VTE_GTK */
+
 static void
 window_decrease_font_size_cb(VteTerminal* terminal,
                              VteappWindow* window)
@@ -1850,7 +2164,12 @@ window_deiconify_window_cb(VteTerminal* terminal,
         if (!options.allow_window_ops)
                 return;
 
+#if VTE_GTK == 3
         gtk_window_deiconify(GTK_WINDOW(window));
+#elif VTE_GTK == 4
+        auto toplevel = GDK_TOPLEVEL(gtk_native_get_surface(GTK_NATIVE(window)));
+        gdk_toplevel_present(toplevel, nullptr); // FIXMEgtk4 nullptr not allowed
+#endif
 }
 
 static void
@@ -1860,26 +2179,25 @@ window_iconify_window_cb(VteTerminal* terminal,
         if (!options.allow_window_ops)
                 return;
 
+#if VTE_GTK == 3
         gtk_window_iconify(GTK_WINDOW(window));
-}
-
-static void
-window_icon_title_changed_cb(VteTerminal* terminal,
-                         VteappWindow* window)
-{
-        if (!options.icon_title)
-                return;
-
-        gdk_window_set_icon_name(gtk_widget_get_window(GTK_WIDGET(window)),
-                                 vte_terminal_get_icon_title(window->terminal));
+#elif VTE_GTK == 4
+        auto toplevel = GDK_TOPLEVEL(gtk_native_get_surface(GTK_NATIVE(window)));
+        gdk_toplevel_minimize(toplevel);
+#endif
 }
 
 static void
 window_window_title_changed_cb(VteTerminal* terminal,
                                VteappWindow* window)
 {
-        gtk_window_set_title(GTK_WINDOW(window),
-                             vte_terminal_get_window_title(window->terminal));
+        auto const title = vte_terminal_get_window_title(window->terminal);
+#if VTE_GTK == 3
+        gtk_window_set_title(GTK_WINDOW(window), title);
+#elif VTE_GTK == 4
+        auto toplevel = GDK_TOPLEVEL(gtk_native_get_surface(GTK_NATIVE(window)));
+        gdk_toplevel_set_title(toplevel, title);
+#endif
 }
 
 static void
@@ -1891,7 +2209,12 @@ window_lower_window_cb(VteTerminal* terminal,
         if (!gtk_widget_get_realized(GTK_WIDGET(window)))
                 return;
 
+#if VTE_GTK == 3
         gdk_window_lower(gtk_widget_get_window(GTK_WIDGET(window)));
+#elif VTE_GTK == 4
+        auto toplevel = GDK_TOPLEVEL(gtk_native_get_surface(GTK_NATIVE(window)));
+        gdk_toplevel_lower(toplevel);
+#endif
 }
 
 static void
@@ -1903,7 +2226,12 @@ window_raise_window_cb(VteTerminal* terminal,
         if (!gtk_widget_get_realized(GTK_WIDGET(window)))
                 return;
 
+#if VTE_GTK == 3
         gdk_window_raise(gtk_widget_get_window(GTK_WIDGET(window)));
+#elif VTE_GTK == 4
+        auto toplevel = GDK_TOPLEVEL(gtk_native_get_surface(GTK_NATIVE(window)));
+        gdk_toplevel_present(toplevel, nullptr); // FIXMEgtk4 gdk_toplevel_raise() doesn't exist??
+#endif
 }
 
 static void
@@ -1935,9 +2263,26 @@ window_move_window_cb(VteTerminal* terminal,
         if (!options.allow_window_ops)
                 return;
 
+#if VTE_GTK == 3
         gtk_window_move(GTK_WINDOW(window), x, y);
+#elif VTE_GTK == 4
+        // FIXMEgtk4
+#endif
+}
+
+#if VTE_GTK == 4
+
+static void
+window_toplevel_notify_state_cb(GdkToplevel* toplevel,
+                                GParamSpec* pspec,
+                                VteappWindow* window)
+{
+        window->toplevel_state = gdk_toplevel_get_state(toplevel);
+        window_update_fullscreen_state(window);
 }
 
+#endif /* VTE_GTK == 4 */
+
 static void
 window_notify_cb(GObject* object,
                  GParamSpec* pspec,
@@ -2047,10 +2392,19 @@ vteapp_window_constructed(GObject *object)
                 gtk_widget_set_margin_bottom(GTK_WIDGET(window->terminal), margin);
         }
 
-        gtk_range_set_adjustment(GTK_RANGE(window->scrollbar),
-                                 gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(window->terminal)));
+        auto vadj = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(window->terminal));
+#if VTE_GTK == 3
+        gtk_range_set_adjustment(GTK_RANGE(window->scrollbar), vadj);
+#elif VTE_GTK == 4
+        gtk_scrollbar_set_adjustment(GTK_SCROLLBAR(window->scrollbar), vadj);
+#endif
+
         if (options.no_scrollbar) {
+#if VTE_GTK == 3
                 gtk_widget_destroy(GTK_WIDGET(window->scrollbar));
+#elif VTE_GTK == 4
+                // FIXMEgtk4
+#endif
                 window->scrollbar = nullptr;
         }
 
@@ -2073,39 +2427,58 @@ vteapp_window_constructed(GObject *object)
         g_action_map_add_action(map, G_ACTION(action.get()));
         g_signal_connect(action.get(), "notify::state", G_CALLBACK(window_input_enabled_state_cb), window);
 
+#if VTE_GTK == 4
+        auto gear_popover = gtk_menu_button_get_popover(GTK_MENU_BUTTON(window->gear_button));
+        gtk_widget_set_halign(GTK_WIDGET(gear_popover), GTK_ALIGN_END);
+#endif
+
         /* Find */
         window->search_popover = vteapp_search_popover_new(window->terminal,
                                                            GTK_WIDGET(window->find_button));
+
         g_signal_connect(window->search_popover, "closed",
                          G_CALLBACK(window_search_popover_closed_cb), window);
         g_signal_connect(window->find_button, "toggled",
                          G_CALLBACK(window_find_button_toggled_cb), window);
 
         /* Clipboard */
+#if VTE_GTK == 3
         window->clipboard = gtk_widget_get_clipboard(GTK_WIDGET(window), GDK_SELECTION_CLIPBOARD);
         g_signal_connect(window->clipboard, "owner-change", G_CALLBACK(window_clipboard_owner_change_cb), 
window);
+#elif VTE_GTK == 4
+        window->clipboard = gtk_widget_get_clipboard(GTK_WIDGET(window));
+        g_signal_connect(window->clipboard, "notify::formats", 
G_CALLBACK(window_clipboard_formats_notify_cb), window);
+#endif /* VTE_GTK */
 
         /* Set ARGB visual */
         if (options.transparency_percent >= 0) {
+#if VTE_GTK == 3
                 if (!options.no_argb_visual) {
                         auto screen = gtk_widget_get_screen(GTK_WIDGET(window));
                         auto visual = gdk_screen_get_rgba_visual(screen);
                         if (visual != nullptr)
                                 gtk_widget_set_visual(GTK_WIDGET(window), visual);
-       }
+                }
 
                 /* Without this transparency doesn't work; see bug #729884. */
                 gtk_widget_set_app_paintable(GTK_WIDGET(window), true);
+
+#elif VTE_GTK == 4
+                // FIXMEgtk4
+#endif /* VTE_GTK == 3 */
         }
 
         /* Signals */
+#if VTE_GTK == 3
         g_signal_connect(window->terminal, "popup-menu", G_CALLBACK(window_popup_menu_cb), window);
         g_signal_connect(window->terminal, "button-press-event", G_CALLBACK(window_button_press_cb), window);
+#elif VTE_GTK == 4
+        // FIXMEgtk4
+#endif
         g_signal_connect(window->terminal, "char-size-changed", G_CALLBACK(window_cell_size_changed_cb), 
window);
         g_signal_connect(window->terminal, "child-exited", G_CALLBACK(window_child_exited_cb), window);
         g_signal_connect(window->terminal, "decrease-font-size", G_CALLBACK(window_decrease_font_size_cb), 
window);
         g_signal_connect(window->terminal, "deiconify-window", G_CALLBACK(window_deiconify_window_cb), 
window);
-        g_signal_connect(window->terminal, "icon-title-changed", G_CALLBACK(window_icon_title_changed_cb), 
window);
         g_signal_connect(window->terminal, "iconify-window", G_CALLBACK(window_iconify_window_cb), window);
         g_signal_connect(window->terminal, "increase-font-size", G_CALLBACK(window_increase_font_size_cb), 
window);
         g_signal_connect(window->terminal, "lower-window", G_CALLBACK(window_lower_window_cb), window);
@@ -2121,11 +2494,13 @@ vteapp_window_constructed(GObject *object)
                 g_signal_connect(window->terminal, "notify", G_CALLBACK(window_notify_cb), window);
 
         /* Settings */
+#if VTE_GTK == 3
         if (options.no_double_buffer) {
                 G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
                 gtk_widget_set_double_buffered(GTK_WIDGET(window->terminal), false);
                 G_GNUC_END_IGNORE_DEPRECATIONS;
         }
+#endif /* VTE_GTK == 3 */
 
         if (options.encoding != nullptr) {
                 auto error = vte::glib::Error{};
@@ -2186,6 +2561,8 @@ vteapp_window_constructed(GObject *object)
         /* Done! */
         gtk_grid_attach(GTK_GRID(window->window_grid), GTK_WIDGET(window->terminal),
                         0, 0, 1, 1);
+        gtk_widget_set_halign(GTK_WIDGET(window->terminal), GTK_ALIGN_FILL);
+        gtk_widget_set_valign(GTK_WIDGET(window->terminal), GTK_ALIGN_FILL);
         gtk_widget_show(GTK_WIDGET(window->terminal));
 
         window_update_paste_sensitivity(window);
@@ -2204,13 +2581,21 @@ vteapp_window_dispose(GObject *object)
 
         if (window->clipboard != nullptr) {
                 g_signal_handlers_disconnect_by_func(window->clipboard,
+#if VTE_GTK == 3
                                                      (void*)window_clipboard_owner_change_cb,
+#elif VTE_GTK == 4
+                                                     (void*)window_clipboard_formats_notify_cb,
+#endif
                                                      window);
                 window->clipboard = nullptr;
         }
 
         if (window->search_popover != nullptr) {
+#if VTE_GTK == 3
                 gtk_widget_destroy(window->search_popover);
+#elif VTE_GTK == 4
+                gtk_widget_unparent(window->search_popover); // this destroys the popover
+#endif /* VTE_GTK */
                 window->search_popover = nullptr;
         }
 
@@ -2225,9 +2610,36 @@ vteapp_window_realize(GtkWidget* widget)
         /* Now we can know the CSD size, and thus apply the geometry. */
         VteappWindow* window = VTEAPP_WINDOW(widget);
         verbose_print("VteappWindow::realize\n");
+
+#if VTE_GTK == 3
+        auto win = gtk_widget_get_window(GTK_WIDGET(window));
+        window->window_state = gdk_window_get_state(win);
+#elif VTE_GTK == 4
+        auto surface = gtk_native_get_surface(GTK_NATIVE(widget));
+        window->toplevel_state = gdk_toplevel_get_state(GDK_TOPLEVEL(surface));
+        g_signal_connect(surface, "notify::state",
+                         G_CALLBACK(window_toplevel_notify_state_cb), window);
+#endif
+
+        window_update_fullscreen_state(window);
+
         vteapp_window_resize(window);
 }
 
+static void
+vteapp_window_unrealize(GtkWidget* widget)
+{
+#if VTE_GTK == 4
+        auto window = VTEAPP_WINDOW(widget);
+        auto toplevel = gtk_native_get_surface(GTK_NATIVE(widget));
+        g_signal_handlers_disconnect_by_func(toplevel,
+                                             (void*)window_toplevel_notify_state_cb,
+                                             window);
+#endif
+
+        GTK_WIDGET_CLASS(vteapp_window_parent_class)->unrealize(widget);
+}
+
 static void
 vteapp_window_show(GtkWidget* widget)
 {
@@ -2239,6 +2651,8 @@ vteapp_window_show(GtkWidget* widget)
         vteapp_window_resize(window);
 }
 
+#if VTE_GTK == 3
+
 static void
 vteapp_window_style_updated(GtkWidget* widget)
 {
@@ -2254,18 +2668,17 @@ static gboolean
 vteapp_window_state_event (GtkWidget* widget,
                            GdkEventWindowState* event)
 {
-        VteappWindow* window = VTEAPP_WINDOW(widget);
+        auto window = VTEAPP_WINDOW(widget);
+        window->window_state = event->new_window_state;
 
-        if (event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN) {
-                window->fullscreen = (event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN) != 0;
-
-                auto action = 
reinterpret_cast<GSimpleAction*>(g_action_map_lookup_action(G_ACTION_MAP(window), "fullscreen"));
-                g_simple_action_set_state(action, g_variant_new_boolean (window->fullscreen));
-        }
+        if (event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN)
+                window_update_fullscreen_state(window);
 
         return GTK_WIDGET_CLASS(vteapp_window_parent_class)->window_state_event(widget, event);
 }
 
+#endif /* VTE_GTK == 3 */
+
 static void
 vteapp_window_class_init(VteappWindowClass* klass)
 {
@@ -2275,9 +2688,15 @@ vteapp_window_class_init(VteappWindowClass* klass)
 
         GtkWidgetClass* widget_class = GTK_WIDGET_CLASS(klass);
         widget_class->realize = vteapp_window_realize;
+        widget_class->unrealize = vteapp_window_unrealize;
         widget_class->show = vteapp_window_show;
+
+#if VTE_GTK == 3
         widget_class->style_updated = vteapp_window_style_updated;
         widget_class->window_state_event = vteapp_window_state_event;
+#elif VTE_GTK == 4
+        // FIXMEgtk4 window state event
+#endif
 
         gtk_widget_class_set_template_from_resource(widget_class, "/org/gnome/vte/app/ui/window.ui");
         gtk_widget_class_set_css_name(widget_class, "vteapp-window");
@@ -2343,8 +2762,14 @@ app_action_close_cb(GSimpleAction* action,
 {
         GtkApplication* application = GTK_APPLICATION(data);
         auto window = gtk_application_get_active_window(application);
-        if (window != nullptr)
-                gtk_widget_destroy(GTK_WIDGET(window));
+        if (window == nullptr)
+                return;
+
+#if VTE_GTK == 3
+        gtk_widget_destroy(GTK_WIDGET(window));
+#elif VTE_GTK == 4
+        gtk_window_destroy(GTK_WINDOW(window));
+#endif
 }
 
 static gboolean
@@ -2378,6 +2803,8 @@ app_stdin_readable_cb(int fd,
         return G_SOURCE_CONTINUE;
 }
 
+#if VTE_GTK == 3
+
 static void
 app_clipboard_targets_received_cb(GtkClipboard* clipboard,
                                   GdkAtom* targets,
@@ -2408,22 +2835,44 @@ app_clipboard_owner_change_cb(GtkClipboard* clipboard,
                                       application);
 }
 
+#elif VTE_GTK == 4
+
+static void
+app_clipboard_changed_cb(GdkClipboard* clipboard,
+                         VteappApplication* application)
+{
+        auto formats = gdk_clipboard_get_formats(clipboard);
+        auto str = vte::glib::take_string(gdk_content_formats_to_string(formats));
+
+        verbose_print("Clipboard owner changed, targets now %s\n", str.get());
+}
+
+#endif /* VTE_GTK */
+
 G_DEFINE_TYPE(VteappApplication, vteapp_application, GTK_TYPE_APPLICATION)
 
 static void
 vteapp_application_init(VteappApplication* application)
 {
         g_object_set(gtk_settings_get_default(),
-                     "gtk-enable-mnemonics", FALSE,
                      "gtk-enable-accels", FALSE,
+#if VTE_GTK == 3
+                     "gtk-enable-mnemonics", FALSE,
                      /* Make gtk+ CSD not steal F10 from the terminal */
                      "gtk-menu-bar-accel", nullptr,
+#endif
                      nullptr);
 
         if (options.css) {
+#if VTE_GTK == 3
                 gtk_style_context_add_provider_for_screen(gdk_screen_get_default (),
                                                           GTK_STYLE_PROVIDER(options.css.get()),
                                                           GTK_STYLE_PROVIDER_PRIORITY_USER);
+#elif VTE_GTK == 4
+                gtk_style_context_add_provider_for_display(gdk_display_get_default (),
+                                                          GTK_STYLE_PROVIDER(options.css.get()),
+                                                          GTK_STYLE_PROVIDER_PRIORITY_USER);
+#endif
         }
 
         if (options.feed_stdin) {
@@ -2435,11 +2884,19 @@ vteapp_application_init(VteappApplication* application)
         }
 
         if (options.track_clipboard_targets) {
+#if VTE_GTK == 3
                 auto clipboard = gtk_clipboard_get_for_display(gdk_display_get_default(),
                                                                GDK_SELECTION_CLIPBOARD);
                 app_clipboard_owner_change_cb(clipboard, nullptr, application);
                 g_signal_connect(clipboard, "owner-change",
                                  G_CALLBACK(app_clipboard_owner_change_cb), application);
+
+#elif VTE_GTK == 4
+                auto clipboard = gdk_display_get_clipboard(gdk_display_get_default());
+                app_clipboard_changed_cb(clipboard, application);
+                g_signal_connect(clipboard, "changed",
+                                 G_CALLBACK(app_clipboard_changed_cb), application);
+#endif /* VTE_GTK */
         }
 }
 
@@ -2532,8 +2989,11 @@ main(int argc,
                return EXIT_SUCCESS;
        }
 
+#if VTE_GTK == 3
        if (options.debug)
                gdk_window_set_debug_updates(true);
+#endif /* VTE_GTK == 3 */
+
 #ifdef VTE_DEBUG
        if (options.test_mode) {
                vte_set_test_flags(VTE_TEST_FLAGS_ALL);
diff --git a/src/app/appmenu-gtk4.ui b/src/app/appmenu-gtk4.ui
new file mode 100644
index 00000000..ac64ebb3
--- /dev/null
+++ b/src/app/appmenu-gtk4.ui
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright © 2017, 2020 Christian Persch
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <https://www.gnu.org/licenses/>.
+-->
+<interface>
+  <menu id="app-menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_New Terminal</attribute>
+        <attribute name="action">app.new</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Close</attribute>
+        <attribute name="action">app.close</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/src/app/meson.build b/src/app/meson.build
index f7df6dbf..dd9c713c 100644
--- a/src/app/meson.build
+++ b/src/app/meson.build
@@ -44,11 +44,11 @@ if get_option('gtk3')
   )
 
   app_gtk3_sources = app_sources + app_gtk3_resource_sources
-  app_gtk3_cppflags = app_common_cppflags + gtk3_version_cppflags
+  app_gtk3_cppflags = app_common_cppflags + gtk3_version_cppflags + ['-DVTE_GTK=3',]
   app_gtk3_deps = app_common_deps + [libvte_gtk3_dep]
 
   app_gtk3 = executable(
-    'vte-' + vte_gtk3_api_version,
+    'vte-' + vte_api_version,
     app_gtk3_sources,
     dependencies: app_gtk3_deps,
     cpp_args: app_gtk3_cppflags,
@@ -56,3 +56,32 @@ if get_option('gtk3')
     install: true,
   )
 endif
+
+if get_option('gtk4')
+
+  app_gtk4_resource_data = files(
+    'appmenu-gtk4.ui',
+    'search-popover-gtk4.ui',
+    'window-gtk4.ui',
+  )
+
+  app_gtk4_resource_sources = gnome.compile_resources(
+    'resources-gtk4.cc',
+    'app-gtk4.gresource.xml',
+    c_name: 'app',
+    dependencies: app_gtk4_resource_data,
+  )
+
+  app_gtk4_sources = app_sources + [app_gtk4_resource_sources,]
+  app_gtk4_cppflags = app_common_cppflags + gtk4_version_cppflags + ['-DVTE_GTK=4',]
+  app_gtk4_deps = app_common_deps + [libvte_gtk4_dep]
+
+  app_gtk4 = executable(
+    'vte-' + vte_api_version + '-gtk4',
+    app_gtk4_sources,
+    dependencies: app_gtk4_deps,
+    cpp_args: app_gtk4_cppflags,
+    include_directories: top_inc,
+    install: true,
+  )
+endif
diff --git a/src/app/search-popover-gtk3.ui b/src/app/search-popover-gtk3.ui
index d7191b86..fbad2d0b 100644
--- a/src/app/search-popover-gtk3.ui
+++ b/src/app/search-popover-gtk3.ui
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.19.0 -->
 <!--
   Copyright © 2016, 2017 Christian Persch
 
diff --git a/src/app/search-popover-gtk4.ui b/src/app/search-popover-gtk4.ui
new file mode 100644
index 00000000..4b187190
--- /dev/null
+++ b/src/app/search-popover-gtk4.ui
@@ -0,0 +1,169 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright © 2016, 2017, 2020 Christian Persch
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <https://www.gnu.org/licenses/>.
+-->
+<interface>
+  <requires lib="gtk+" version="3.16"/>
+  <template class="VteappSearchPopover" parent="GtkPopover">
+    <property name="can_focus">0</property>
+    <property name="child">
+      <object class="GtkBox" id="box1">
+        <property name="can_focus">0</property>
+        <property name="margin-start">12</property>
+        <property name="margin-end">12</property>
+        <property name="margin_top">12</property>
+        <property name="margin_bottom">12</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkBox" id="box2">
+            <property name="can_focus">0</property>
+            <property name="spacing">18</property>
+            <child>
+              <object class="GtkBox" id="box4">
+                <property name="hexpand">1</property>
+                <property name="can_focus">0</property>
+                <child>
+                  <object class="GtkSearchEntry" id="search_entry">
+                    <property name="hexpand">1</property>
+                    <property name="activates_default">1</property>
+                    <property name="width_chars">30</property>
+                    <property name="placeholder_text" translatable="yes">Search</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkButton" id="search_prev_button">
+                    <property name="receives_default">1</property>
+                    <property name="tooltip_text" translatable="yes">Search for previous 
occurrence</property>
+                    <property name="focus_on_click">0</property>
+                    <child>
+                      <object class="GtkImage" id="image2">
+                        <property name="can_focus">0</property>
+                        <property name="icon_name">go-up-symbolic</property>
+                        <property name="use_fallback">1</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkButton" id="search_next_button">
+                    <property name="receives_default">1</property>
+                    <property name="tooltip_text" translatable="yes">Search for next occurrence</property>
+                    <property name="focus_on_click">0</property>
+                    <child>
+                      <object class="GtkImage" id="image3">
+                        <property name="can_focus">0</property>
+                        <property name="icon_name">go-down-symbolic</property>
+                        <property name="use_fallback">1</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <style>
+                  <class name="linked"/>
+                </style>
+              </object>
+            </child>
+            <child>
+              <object class="GtkToggleButton" id="reveal_button">
+                <property name="receives_default">1</property>
+                <property name="tooltip_text" translatable="yes">Toggle search options</property>
+                <property name="focus_on_click">0</property>
+                <child>
+                  <object class="GtkImage" id="image1">
+                    <property name="can_focus">0</property>
+                    <property name="icon_name">open-menu-symbolic</property>
+                    <property name="use_fallback">1</property>
+                  </object>
+                </child>
+               <!--
+                <accessibility>
+                  <relation type="controller-for" target="revealer"/>
+                </accessibility>
+               -->
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="close_button">
+                <property name="receives_default">1</property>
+                <property name="focus_on_click">0</property>
+                <child>
+                  <object class="GtkImage" id="image4">
+                    <property name="can_focus">0</property>
+                    <property name="icon_name">window-close-symbolic</property>
+                    <property name="use_fallback">1</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkRevealer" id="revealer">
+            <property name="can_focus">0</property>
+            <property name="transition_type">none</property>
+            <property name="reveal_child">0</property>
+            <property name="child">
+              <object class="GtkBox" id="box3">
+                <property name="can_focus">0</property>
+                <property name="margin_top">18</property>
+                <property name="orientation">vertical</property>
+                <property name="spacing">6</property>
+                <child>
+                  <object class="GtkCheckButton" id="match_case_checkbutton">
+                    <property name="valign">center</property>
+                    <property name="label" translatable="yes">_Match case</property>
+                    <property name="use_underline">1</property>
+                    <property name="focus_on_click">0</property>
+                    <property name="halign">start</property>
+                    <property name="valign">center</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkCheckButton" id="entire_word_checkbutton">
+                    <property name="valign">center</property>
+                    <property name="label" translatable="yes">Match _entire word only</property>
+                    <property name="use_underline">1</property>
+                    <property name="focus_on_click">0</property>
+                    <property name="halign">start</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkCheckButton" id="regex_checkbutton">
+                    <property name="valign">center</property>
+                    <property name="label" translatable="yes">Match as _regular expression</property>
+                    <property name="use_underline">1</property>
+                    <property name="focus_on_click">0</property>
+                    <property name="halign">start</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkCheckButton" id="wrap_around_checkbutton">
+                    <property name="valign">center</property>
+                    <property name="label" translatable="yes">_Wrap around</property>
+                    <property name="use_underline">1</property>
+                    <property name="focus_on_click">0</property>
+                    <property name="halign">start</property>
+                    <property name="active">1</property>
+                  </object>
+                </child>
+              </object>
+            </property>
+          </object>
+        </child>
+      </object>
+    </property>
+  </template>
+</interface>
diff --git a/src/app/window-gtk3.ui b/src/app/window-gtk3.ui
index 29758b1a..b7bdb012 100644
--- a/src/app/window-gtk3.ui
+++ b/src/app/window-gtk3.ui
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.19.0 -->
 <!--
   Copyright © 2014, 2017 Christian Persch
 
diff --git a/src/app/window-gtk4.ui b/src/app/window-gtk4.ui
new file mode 100644
index 00000000..dc3c33a4
--- /dev/null
+++ b/src/app/window-gtk4.ui
@@ -0,0 +1,185 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright © 2014, 2017, 2020 Christian Persch
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <https://www.gnu.org/licenses/>.
+-->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <menu id="gear_menu_model">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_New Terminal</attribute>
+        <attribute name="action">app.new</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Copy</attribute>
+        <attribute name="action">win.copy</attribute>
+        <attribute name="target">text</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Copy As _HTML</attribute>
+        <attribute name="action">win.copy</attribute>
+        <attribute name="target">html</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Paste</attribute>
+        <attribute name="action">win.paste</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Find…</attribute>
+        <attribute name="action">win.find</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Reset</attribute>
+        <attribute name="action">win.reset</attribute>
+        <attribute name="target" type="b">false</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Reset and Cl_ear</attribute>
+        <attribute name="action">win.reset</attribute>
+        <attribute name="target" type="b">true</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Input enabled</attribute>
+        <attribute name="action">win.input-enabled</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Fullscreen</attribute>
+        <attribute name="action">win.fullscreen</attribute>
+      </item>
+    </section>
+  </menu>
+  <template class="VteappWindow" parent="GtkApplicationWindow">
+    <property name="can_focus">0</property>
+    <property name="icon_name">utilities-terminal</property>
+    <child>
+      <object class="GtkGrid" id="window_grid">
+        <property name="can_focus">0</property>
+        <property name="halign">fill</property>
+        <property name="valign">fill</property>
+        <property name="hexpand">1</property>
+        <property name="vexpand">1</property>
+        <child>
+          <placeholder/>
+        </child>
+        <child>
+          <object class="GtkScrollbar" id="scrollbar">
+            <property name="can_focus">0</property>
+            <property name="orientation">vertical</property>
+            <property name="hexpand">0</property>
+            <property name="vexpand">1</property>
+            <layout>
+              <property name="column">1</property>
+              <property name="row">0</property>
+            </layout>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child type="titlebar">
+      <object class="GtkHeaderBar" id="headerbar">
+        <property name="can_focus">0</property>
+        <property name="decoration_layout">:close</property>
+        <child type="start">
+          <object class="GtkButton" id="copy_button">
+            <property name="receives_default">1</property>
+            <property name="tooltip_text" translatable="yes">Copy</property>
+            <property name="action_name">win.copy</property>
+            <property name="action_target">&quot;text&quot;</property>
+            <property name="focus_on_click">0</property>
+            <child>
+              <object class="GtkImage" id="image2">
+                <property name="can_focus">0</property>
+                <property name="icon_name">edit-copy-symbolic</property>
+                <property name="use_fallback">1</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child type="start">
+          <object class="GtkButton" id="paste_button">
+            <property name="receives_default">1</property>
+            <property name="tooltip_text" translatable="yes">Paste</property>
+            <property name="action_name">win.paste</property>
+            <property name="focus_on_click">0</property>
+            <child>
+              <object class="GtkImage" id="image3">
+                <property name="can_focus">0</property>
+                <property name="icon_name">edit-paste-symbolic</property>
+                <property name="use_fallback">1</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child type="start">
+          <object class="GtkToggleButton" id="find_button">
+            <property name="receives_default">1</property>
+            <property name="focus_on_click">0</property>
+            <child>
+              <object class="GtkImage" id="image5">
+                <property name="can_focus">0</property>
+                <property name="icon_name">edit-find-symbolic</property>
+                <property name="use_fallback">1</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child type="title">
+          <placeholder/>
+        </child>
+        <child type="end">
+          <object class="GtkMenuButton" id="gear_button">
+            <property name="receives_default">1</property>
+            <property name="focus_on_click">0</property>
+            <property name="menu-model">gear_menu_model</property>
+            <property name="icon_name">open-menu-symbolic</property>
+          </object>
+        </child>
+        <child type="end">
+          <object class="GtkGrid" id="notifications_grid">
+            <property name="can_focus">0</property>
+            <property name="column_spacing">6</property>
+            <property name="row_spacing">6</property>
+            <property name="hexpand">0</property>
+            <property name="vexpand">1</property>
+            <child>
+              <object class="GtkImage" id="readonly_emblem">
+                <property name="visible">0</property>
+                <property name="can_focus">0</property>
+                <property name="tooltip_text" translatable="yes">Read-only</property>
+                <property name="icon_name">emblem-readonly</property>
+                <property name="use_fallback">1</property>
+                <property name="hexpand">0</property>
+                <property name="vexpand">0</property>
+                <layout>
+                  <property name="column">0</property>
+                  <property name="row">0</property>
+                </layout>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/cairo-glue.hh b/src/cairo-glue.hh
index 234c8fcd..c6370e33 100644
--- a/src/cairo-glue.hh
+++ b/src/cairo-glue.hh
@@ -23,7 +23,9 @@
 
 namespace vte {
 
-VTE_DECLARE_FREEABLE(cairo_t, cairo_destroy);
+VTE_DECLARE_FREEABLE(cairo_rectangle_list_t, cairo_rectangle_list_destroy);
+VTE_DECLARE_FREEABLE(cairo_region_t, cairo_region_destroy);
 VTE_DECLARE_FREEABLE(cairo_surface_t, cairo_surface_destroy);
+VTE_DECLARE_FREEABLE(cairo_t, cairo_destroy);
 
 } // namespace vte::cairo
diff --git a/src/clipboard-gtk.cc b/src/clipboard-gtk.cc
index a70c0eea..402e3a41 100644
--- a/src/clipboard-gtk.cc
+++ b/src/clipboard-gtk.cc
@@ -22,6 +22,7 @@
 #include "widget.hh"
 #include "vteinternal.hh"
 
+#include <new>
 #include <stdexcept>
 #include <utility>
 
@@ -42,12 +43,20 @@ Clipboard::Clipboard(Widget& delegate,
 
         switch (type) {
         case ClipboardType::PRIMARY:
-                m_clipboard = vte::glib::make_ref(gtk_clipboard_get_for_display(display,
-                                                                                GDK_SELECTION_PRIMARY));
+                m_clipboard = vte::glib::make_ref
+#if VTE_GTK == 3
+                        (gtk_clipboard_get_for_display(display, GDK_SELECTION_PRIMARY));
+#elif VTE_GTK == 4
+                        (gdk_display_get_primary_clipboard(display));
+#endif
                 break;
         case ClipboardType::CLIPBOARD:
-                m_clipboard = vte::glib::make_ref(gtk_clipboard_get_for_display(display,
-                                                                                GDK_SELECTION_CLIPBOARD));
+                m_clipboard = vte::glib::make_ref
+#if VTE_GTK == 3
+                        (gtk_clipboard_get_for_display(display, GDK_SELECTION_CLIPBOARD));
+#elif VTE_GTK == 4
+                        (gdk_display_get_clipboard(display));
+#endif
                 break;
         }
 
@@ -55,6 +64,8 @@ Clipboard::Clipboard(Widget& delegate,
                 throw std::runtime_error{"Failed to create clipboard"};
 }
 
+#if VTE_GTK == 3
+
 class Clipboard::Offer {
 public:
         Offer(Clipboard& clipboard,
@@ -75,7 +86,7 @@ public:
         {
                 auto [targets, n_targets] = targets_for_format(format);
 
-                // Transfers clipboardship of *offer to the clipboard. If setting succeeds,
+                // Transfers ownership of *offer to the clipboard. If setting succeeds,
                 // the clipboard will own *offer until the clipboard_data_clear_cb
                 // callback is called.
                 // If setting the clipboard fails, the clear callback will never be
@@ -150,8 +161,8 @@ private:
                          guint info,
                          void* user_data) noexcept
         {
-                if (info != vte::to_integral(ClipboardFormat::TEXT) &&
-                    info != vte::to_integral(ClipboardFormat::HTML))
+                if (int(info) != vte::to_integral(ClipboardFormat::TEXT) &&
+                    int(info) != vte::to_integral(ClipboardFormat::HTML))
                         return;
 
                 reinterpret_cast<Offer*>(user_data)->dispatch_get(ClipboardFormat(info), data);
@@ -161,12 +172,11 @@ private:
         clipboard_clear_cb(GtkClipboard* clipboard,
                            void* user_data) noexcept
         {
-                // Assume ownership of the Request, and delete it after dispatching the callback
+                // Assume ownership of the Offer, and delete it after dispatching the callback
                 auto offer = std::unique_ptr<Offer>{reinterpret_cast<Offer*>(user_data)};
                 offer->dispatch_clear();
         }
 
-
         static std::pair<GtkTargetEntry*, int>
         targets_for_format(ClipboardFormat format)
         {
@@ -227,6 +237,8 @@ private:
 
 }; // class Clipboard::Offer
 
+#endif /* VTE_GTK == 3 */
+
 class Clipboard::Request {
 public:
         Request(Clipboard& clipboard,
@@ -244,10 +256,12 @@ public:
 
         static void run(std::unique_ptr<Request> request) noexcept
         {
+#if VTE_GTK == 3
                 auto platform = request->clipboard().platform();
                 gtk_clipboard_request_text(platform,
                                            text_received_cb,
                                            request.release());
+#endif /* VTE_GTK */
         }
 
 private:
@@ -255,6 +269,7 @@ private:
         RequestDoneCallback m_done_callback;
         RequestFailedCallback m_failed_callback;
 
+#if VTE_GTK == 3
         void dispatch(char const *text) noexcept
         try
         {
@@ -278,6 +293,8 @@ private:
                 request->dispatch(text);
         }
 
+#endif /* VTE_GTK */
+
 }; // class Clipboard::Request
 
 void
@@ -285,13 +302,17 @@ Clipboard::offer_data(ClipboardFormat format,
                       OfferGetCallback get_callback,
                       OfferClearCallback clear_callback) /* throws */
 {
+#if VTE_GTK == 3
         Offer::run(std::make_unique<Offer>(*this, get_callback, clear_callback), format);
+#endif
 }
 
 void
 Clipboard::set_text(std::string_view const& text) noexcept
 {
+#if VTE_GTK == 3
         gtk_clipboard_set_text(platform(), text.data(), text.size());
+#endif
 }
 
 void
diff --git a/src/clipboard-gtk.hh b/src/clipboard-gtk.hh
index d2b66228..45660c3e 100644
--- a/src/clipboard-gtk.hh
+++ b/src/clipboard-gtk.hh
@@ -75,7 +75,11 @@ public:
                           RequestFailedCallback failed_callback) /* throws */;
 
 private:
+#if VTE_GTK == 3
         vte::glib::RefPtr<GtkClipboard> m_clipboard;
+#elif VTE_GTK == 4
+        vte::glib::RefPtr<GdkClipboard> m_clipboard;
+#endif
         std::weak_ptr<Widget> m_delegate;
         ClipboardType m_type;
 
diff --git a/src/debug.h b/src/debug.h
index c91c4968..da1d51d8 100644
--- a/src/debug.h
+++ b/src/debug.h
@@ -113,6 +113,12 @@ static void _vte_debug_print(guint flags, const char *fmt, ...)
 #define _vte_debug_print(args...) do { } while(0)
 #endif /* VTE_DEBUG */
 
+static inline char const*
+_vte_debug_tf(bool v) noexcept
+{
+        return v ? "true" : "false";
+}
+
 G_END_DECLS
 
 #endif
diff --git a/src/fonts-pangocairo.cc b/src/fonts-pangocairo.cc
index fc0f4e54..e77981b5 100644
--- a/src/fonts-pangocairo.cc
+++ b/src/fonts-pangocairo.cc
@@ -210,13 +210,13 @@ FontInfo::measure_font()
        }
 }
 
-FontInfo::FontInfo(PangoContext *context)
+FontInfo::FontInfo(vte::glib::RefPtr<PangoContext> context)
 {
        _vte_debug_print (VTE_DEBUG_PANGOCAIRO,
                          "vtepangocairo: %p allocating FontInfo\n",
                          (void*)this);
 
-       m_layout = vte::glib::take_ref(pango_layout_new(context));
+       m_layout = vte::glib::take_ref(pango_layout_new(context.get()));
 
        auto tabs = pango_tab_array_new_with_positions(1, FALSE, PANGO_TAB_LEFT, 1);
        pango_layout_set_tabs(m_layout.get(), tabs);
@@ -230,7 +230,7 @@ FontInfo::FontInfo(PangoContext *context)
 #if PANGO_VERSION_CHECK(1, 44, 0)
         /* Try using the font's metrics; see issue#163. */
         if (auto metrics = vte::take_freeable
-            (pango_context_get_metrics(context,
+            (pango_context_get_metrics(context.get(),
                                        nullptr /* use font from context */,
                                        nullptr /* use language from context */))) {
                /* Use provided metrics if possible */
@@ -337,7 +337,7 @@ context_equal (PangoContext *a,
            && vte_pango_context_get_fontconfig_timestamp (a) == vte_pango_context_get_fontconfig_timestamp 
(b);
 }
 
-/* assumes ownership/reference of context */
+// FIXMEchpe return vte::base::RefPtr<FontInfo>
 FontInfo*
 FontInfo::create_for_context(vte::glib::RefPtr<PangoContext> context,
                              PangoFontDescription const* desc,
@@ -381,14 +381,13 @@ FontInfo::create_for_context(vte::glib::RefPtr<PangoContext> context,
                                  info);
                info = info->ref();
        } else {
-               _vte_debug_print (VTE_DEBUG_PANGOCAIRO,
-                                 "vtepangocairo: FontInfo not in cache\n");
-                info = new FontInfo{context.get()};
-        }
+                info = new FontInfo{std::move(context)};
+       }
 
        return info;
 }
 
+#if VTE_GTK == 3
 FontInfo*
 FontInfo::create_for_screen(GdkScreen* screen,
                             PangoFontDescription const* desc,
@@ -400,15 +399,28 @@ FontInfo::create_for_screen(GdkScreen* screen,
        return create_for_context(vte::glib::take_ref(gdk_pango_context_get_for_screen(screen)),
                                   desc, language, fontconfig_timestamp);
 }
+#endif /* VTE_GTK */
 
 FontInfo*
 FontInfo::create_for_widget(GtkWidget* widget,
                             PangoFontDescription const* desc)
 {
-       auto screen = gtk_widget_get_screen(widget);
-       auto language = pango_context_get_language(gtk_widget_get_pango_context(widget));
+        auto context = gtk_widget_get_pango_context(widget);
+        auto language = pango_context_get_language(context);
 
+#if VTE_GTK == 3
+       auto screen = gtk_widget_get_screen(widget);
        return create_for_screen(screen, desc, language);
+#elif VTE_GTK == 4
+        auto display = gtk_widget_get_display(widget);
+        auto settings = gtk_settings_get_for_display(display);
+        auto fontconfig_timestamp = guint{};
+        g_object_get (settings, "gtk-fontconfig-timestamp", &fontconfig_timestamp, nullptr);
+        return create_for_context(vte::glib::make_ref(context),
+                                  desc, language, fontconfig_timestamp);
+        // FIXMEgtk4: this uses a per-widget context, while the gtk3 code uses a per-screen
+        // one. That means there may be a lot less sharing and a lot more FontInfo's around?
+#endif
 }
 
 FontInfo::UnistrInfo*
diff --git a/src/fonts-pangocairo.hh b/src/fonts-pangocairo.hh
index c7aac75c..586e6668 100644
--- a/src/fonts-pangocairo.hh
+++ b/src/fonts-pangocairo.hh
@@ -120,7 +120,7 @@ class FontInfo {
         int const font_cache_timeout = 30; // seconds
 
 public:
-        FontInfo(PangoContext* context);
+        FontInfo(vte::glib::RefPtr<PangoContext> context);
         ~FontInfo();
 
         FontInfo* ref()
@@ -269,9 +269,12 @@ private:
                                             PangoFontDescription const* desc,
                                             PangoLanguage* language,
                                             guint fontconfig_timestamp);
+#if VTE_GTK == 3
         static FontInfo *create_for_screen(GdkScreen* screen,
                                            PangoFontDescription const* desc,
                                            PangoLanguage* language);
+#endif
+
 public:
 
         static FontInfo *create_for_widget(GtkWidget* widget,
diff --git a/src/graphene-glue.hh b/src/graphene-glue.hh
new file mode 100644
index 00000000..16456117
--- /dev/null
+++ b/src/graphene-glue.hh
@@ -0,0 +1,48 @@
+/*
+ * Copyright © 2020 Christian Persch
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <cairo.h>
+#include <graphene.h>
+
+#include "std-glue.hh"
+
+namespace vte::graphene {
+
+inline constexpr auto
+make_rect(int x,
+          int y,
+          int width,
+          int height)
+{
+        return GRAPHENE_RECT_INIT(float(x), float(y), float(width), float(height));
+}
+
+inline constexpr auto
+make_rect(cairo_rectangle_int_t const* rect)
+{
+        return make_rect(rect->x, rect->y, rect->width, rect->height);
+}
+
+} // namespace vte::graphene
+
+namespace vte {
+
+// VTE_DECLARE_FREEABLE(graphene_rect_t, graphene_rect_free);
+
+} // namespace vte::cairo
diff --git a/src/gtk-glue.hh b/src/gtk-glue.hh
index 1634beb6..71ffc9fd 100644
--- a/src/gtk-glue.hh
+++ b/src/gtk-glue.hh
@@ -25,6 +25,12 @@ namespace vte::gtk {
 
 namespace vte {
 
+#if VTE_GTK == 3
 VTE_DECLARE_FREEABLE(GtkTargetList, gtk_target_list_unref);
+#endif /* VTE_GTK == 3 */
+
+#if VTE_GTK == 4
+VTE_DECLARE_FREEABLE(GdkContentFormats, gdk_content_formats_unref);
+#endif /* VTE_GTK == 4 */
 
 } // namespace vte
diff --git a/src/keymap.h b/src/keymap.h
index 3feef90e..4eface40 100644
--- a/src/keymap.h
+++ b/src/keymap.h
@@ -25,8 +25,13 @@
 
 G_BEGIN_DECLS
 
+#if VTE_GTK == 3
 #define VTE_ALT_MASK           GDK_MOD1_MASK
 #define VTE_NUMLOCK_MASK       GDK_MOD2_MASK
+#elif VTE_GTK == 4
+#define VTE_ALT_MASK           GDK_ALT_MASK
+#define VTE_NUMLOCK_MASK       0 /* FIXME */
+#endif
 
 /* Map the specified keyval/modifier setup, dependent on the mode, to either
  * a literal string or a capability name. */
diff --git a/src/meson.build b/src/meson.build
index bfbeebb2..e9192be0 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -32,6 +32,10 @@ glib_glue_sources = files(
   'glib-glue.hh',
 )
 
+graphene_glue_sources = files(
+  'graphene-glue.hh',
+)
+
 gtk_glue_sources = files(
   'gtk-glue.hh',
 )
@@ -212,10 +216,6 @@ libvte_common_sources = config_sources + debug_sources + glib_glue_sources + gtk
   'widget.hh',
 )
 
-if get_option('a11y')
-  libvte_common_sources += a11y_sources
-endif
-
 if get_option('icu')
   libvte_common_sources += icu_sources
 endif
@@ -294,9 +294,13 @@ libvte_common_cppflags = [
 
 if get_option('gtk3')
   libvte_gtk3_sources = libvte_common_sources + libvte_gtk3_public_headers + libvte_gtk3_enum_sources
-  libvte_gtk3_cppflags = libvte_common_cppflags + gtk3_version_cppflags
-  libvte_gtk3_deps = libvte_common_deps + [gtk3_dep]
-  libvte_gtk3_public_deps = libvte_common_public_deps + [gtk3_dep]
+  libvte_gtk3_cppflags = libvte_common_cppflags + gtk3_version_cppflags + ['-DVTE_GTK=3',]
+  libvte_gtk3_deps = libvte_common_deps + [gtk3_dep,]
+  libvte_gtk3_public_deps = libvte_common_public_deps + [gtk3_dep,]
+
+  if get_option('a11y')
+    libvte_gtk3_sources += a11y_sources
+  endif
 
   libvte_gtk3 = shared_library(
     vte_gtk3_api_name,
@@ -310,7 +314,7 @@ if get_option('gtk3')
 
   libvte_gtk3_dep = declare_dependency(
     sources: libvte_gtk3_public_headers,
-    include_directories: [src_inc, vte_inc],
+    include_directories: [src_inc, vte_inc,],
     dependencies: libvte_gtk3_deps,
     link_with: libvte_gtk3
   )
@@ -327,6 +331,41 @@ if get_option('gtk3')
   )
 endif
 
+if get_option('gtk4')
+  libvte_gtk4_sources = libvte_common_sources + libvte_gtk4_public_headers + libvte_gtk4_enum_sources + 
graphene_glue_sources
+  libvte_gtk4_cppflags = libvte_common_cppflags + gtk4_version_cppflags + ['-DVTE_GTK=4',]
+  libvte_gtk4_deps = libvte_common_deps + [gtk4_dep,]
+  libvte_gtk4_public_deps = libvte_common_public_deps + [gtk4_dep,]
+
+  libvte_gtk4 = shared_library(
+    vte_gtk4_api_name,
+    sources: libvte_gtk4_sources,
+    version: libvte_gtk4_soversion,
+    include_directories: incs,
+    dependencies: libvte_gtk4_deps,
+    cpp_args: libvte_gtk4_cppflags,
+    install: true,
+  )
+
+  libvte_gtk4_dep = declare_dependency(
+    sources: libvte_gtk4_public_headers,
+    include_directories: [src_inc, vte_inc,],
+    dependencies: libvte_gtk4_deps,
+    link_with: libvte_gtk4
+  )
+
+  pkg.generate(
+    libvte_gtk4,
+    version: vte_version,
+    name: 'vte',
+    description: 'VTE widget for GTK+ 4.0',
+    filebase: vte_gtk4_api_name,
+    subdirs: vte_gtk4_api_name,
+    requires: libvte_gtk4_public_deps,
+    variables: 'exec_prefix=${prefix}',
+  )
+endif
+
 ## Tests
 
 # decoder cat
@@ -425,17 +464,19 @@ reflect_textview = executable(
   install: false,
 )
 
-reflect_vte = executable(
-  'reflect-vte',
-  sources: reflect_sources,
-  dependencies: [gtk3_dep, libvte_gtk3_dep],
-  c_args: [
-    '-DUSE_VTE',
-    '-DVTE_DISABLE_DEPRECATION_WARNINGS',
-  ],
-  include_directories: top_inc,
-  install: false,
-)
+if get_option('gtk3')
+  reflect_vte = executable(
+    'reflect-vte',
+    sources: reflect_sources,
+    dependencies: [gtk3_dep, libvte_gtk3_dep,],
+    c_args: [
+      '-DUSE_VTE',
+      '-DVTE_DISABLE_DEPRECATION_WARNINGS',
+    ],
+    include_directories: [top_inc,],
+    install: false,
+  )
+endif
 
 # vte-urlencode-cwd
 
@@ -599,14 +640,16 @@ test_vtetypes_sources = config_sources + libc_glue_sources + files(
    'vtetypes.hh',
 )
 
-test_vtetypes = executable(
-  'test-vtetypes',
-  sources: test_vtetypes_sources,
-  dependencies: [glib_dep, pango_dep, gtk3_dep],
-  cpp_args: ['-DMAIN'],
-  include_directories: top_inc,
-  install: false,
-)
+if get_option('gtk3')
+  test_vtetypes = executable(
+    'test-vtetypes',
+    sources: test_vtetypes_sources,
+    dependencies: [glib_dep, pango_dep, gtk3_dep,],
+    cpp_args: ['-DMAIN'],
+    include_directories: top_inc,
+    install: false,
+  )
+endif
 
 test_env = [
   'VTE_DEBUG=0'
@@ -621,9 +664,14 @@ test_units = [
   ['stream', test_stream],
   ['tabstops', test_tabstops],
   ['utf8', test_utf8],
-  ['vtetypes', test_vtetypes],
 ]
 
+if get_option('gtk3')
+  test_units += [
+    ['vtetypes', test_vtetypes],
+  ]
+endif
+
 if get_option('sixel')
   test_units += [
     ['sixel', test_sixel],
diff --git a/src/vte.cc b/src/vte.cc
index 64193498..d9a2ec3d 100644
--- a/src/vte.cc
+++ b/src/vte.cc
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2001-2004,2009,2010 Red Hat, Inc.
- * Copyright © 2008, 2009, 2010 Christian Persch
+ * Copyright © 2008, 2009, 2010, 2020 Christian Persch
  *
  * This library is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published
@@ -49,6 +49,7 @@
 #include "ringview.hh"
 #include "caps.hh"
 #include "widget.hh"
+#include "cairo-glue.hh"
 
 #ifdef HAVE_WCHAR_H
 #include <wchar.h>
@@ -72,8 +73,12 @@
 #include "gobject-glue.hh"
 
 #ifdef WITH_A11Y
+#if VTE_GTK == 3
 #include "vteaccess.h"
-#endif
+#else
+#undef WITH_A11Y
+#endif /* VTE_GTK == 3 */
+#endif /* WITH_A11Y */
 
 #include <new> /* placement new */
 
@@ -95,6 +100,12 @@ static inline double round(double x) {
 
 #define VTE_DRAW_OPAQUE (1.0)
 
+#if VTE_GTK == 3
+#define VTE_STYLE_CLASS_READ_ONLY GTK_STYLE_CLASS_READ_ONLY
+#elif VTE_GTK == 4
+#define VTE_STYLE_CLASS_READ_ONLY "read-only"
+#endif
+
 namespace vte {
 namespace terminal {
 
@@ -106,7 +117,10 @@ static void remove_update_timeout(vte::terminal::Terminal* that);
 
 static gboolean process_timeout (gpointer data) noexcept;
 static gboolean update_timeout (gpointer data) noexcept;
-static cairo_region_t *vte_cairo_get_clip_region (cairo_t *cr);
+
+#if VTE_GTK == 3
+static vte::Freeable<cairo_region_t> vte_cairo_get_clip_region(cairo_t* cr);
+#endif
 
 /* these static variables are guarded by the GDK mutex */
 static guint process_timeout_tag = 0;
@@ -438,7 +452,11 @@ Terminal::invalidate_rows(vte::grid::row_t row_start,
                 rect.x += allocation.x + m_padding.left;
                 rect.y += allocation.y + m_padding.top;
                 cairo_region_t *region = cairo_region_create_rectangle(&rect);
+#if VTE_GTK == 3
                gtk_widget_queue_draw_region(m_widget, region);
+#elif VTE_GTK == 4
+                gtk_widget_queue_draw(m_widget); // FIXMEgtk4
+#endif
                 cairo_region_destroy(region);
        }
 
@@ -1528,6 +1546,9 @@ Terminal::regex_match_check(vte::grid::column_t column,
                             vte::grid::row_t row,
                             int* tag)
 {
+        /* Need to ensure the ringview is updated. */
+        ringview_update();
+
        long delta = m_screen->scroll_delta;
        _vte_debug_print(VTE_DEBUG_EVENTS | VTE_DEBUG_REGEX,
                        "Checking for match at (%ld,%ld).\n",
@@ -1568,7 +1589,11 @@ Terminal::regex_match_check(vte::grid::column_t column,
 vte::view::coords
 Terminal::view_coords_from_event(vte::platform::MouseEvent const& event) const
 {
+#if VTE_GTK == 3
         return vte::view::coords(event.x() - m_padding.left, event.y() - m_padding.top);
+#elif VTE_GTK == 4
+        return vte::view::coords(event.x(), event.y());
+#endif
 }
 
 bool
@@ -1816,10 +1841,50 @@ Terminal::rowcol_from_event(vte::platform::MouseEvent const& event,
         return true;
 }
 
-char *
+#if VTE_GTK == 4
+
+bool
+Terminal::rowcol_at(double x,
+                    double y,
+                    long* column,
+                    long* row)
+{
+        auto rowcol = grid_coords_from_view_coords(vte::view::coords(x, y));
+        if (!grid_coords_visible(rowcol))
+                return false;
+
+        *column = rowcol.column();
+        *row = rowcol.row();
+        return true;
+}
+
+char*
+Terminal::hyperlink_check_at(double x,
+                             double y)
+{
+        long col, row;
+        if (!rowcol_at(x, y, &col, &row))
+                return nullptr;
+
+        return hyperlink_check(col, row);
+}
+
+#endif /* VTE_GTK == 4 */
+
+char*
 Terminal::hyperlink_check(vte::platform::MouseEvent const& event)
 {
         long col, row;
+        if (!rowcol_from_event(event, &col, &row))
+                return nullptr;
+
+        return hyperlink_check(col, row);
+}
+
+char*
+Terminal::hyperlink_check(vte::grid::column_t col,
+                          vte::grid::row_t row)
+{
         const char *hyperlink;
         const char *separator;
 
@@ -1829,9 +1894,6 @@ Terminal::hyperlink_check(vte::platform::MouseEvent const& event)
         /* Need to ensure the ringview is updated. */
         ringview_update();
 
-        if (!rowcol_from_event(event, &col, &row))
-                return NULL;
-
         _vte_ring_get_hyperlink_at_position(m_screen->row_data, row, col, false, &hyperlink);
 
         if (hyperlink != NULL) {
@@ -1848,15 +1910,12 @@ Terminal::hyperlink_check(vte::platform::MouseEvent const& event)
         return g_strdup(hyperlink);
 }
 
-char *
+char*
 Terminal::regex_match_check(vte::platform::MouseEvent const& event,
                             int *tag)
 {
         long col, row;
 
-        /* Need to ensure the ringview is updated. */
-        ringview_update();
-
         if (!rowcol_from_event(event, &col, &row))
                 return FALSE;
 
@@ -1871,10 +1930,24 @@ Terminal::regex_match_check_extra(vte::platform::MouseEvent const& event,
                                   size_t n_regexes,
                                   uint32_t match_flags,
                                   char** matches)
+{
+        long col, row;
+        if (!rowcol_from_event(event, &col, &row))
+                return false;
+
+        return regex_match_check_extra(col, row, regexes, n_regexes, match_flags, matches);
+}
+
+bool
+Terminal::regex_match_check_extra(vte::grid::column_t col,
+                                  vte::grid::row_t row,
+                                  vte::base::Regex const** regexes,
+                                  size_t n_regexes,
+                                  uint32_t match_flags,
+                                  char** matches)
 {
        gsize offset, sattr, eattr;
         bool any_matches = false;
-        long col, row;
         guint i;
 
         assert(regexes != nullptr || n_regexes == 0);
@@ -1883,9 +1956,6 @@ Terminal::regex_match_check_extra(vte::platform::MouseEvent const& event,
         /* Need to ensure the ringview is updated. */
         ringview_update();
 
-        if (!rowcol_from_event(event, &col, &row))
-                return false;
-
        if (m_match_contents == nullptr) {
                match_contents_refresh();
        }
@@ -4583,10 +4653,11 @@ Terminal::beep()
 bool
 Terminal::widget_key_press(vte::platform::KeyEvent const& event)
 {
+        auto handled = false;
        char *normal = NULL;
        gsize normal_length = 0;
        struct termios tio;
-       gboolean scrolled = FALSE, steal = FALSE, modifier = FALSE, handled,
+       gboolean scrolled = FALSE, steal = FALSE, modifier = FALSE,
                 suppress_alt_esc = FALSE, add_modifiers = FALSE;
        guint keyval = 0;
        gunichar keychar = 0;
@@ -4614,12 +4685,6 @@ Terminal::widget_key_press(vte::platform::KeyEvent const& event)
                         set_pointer_autohidden(true);
                }
 
-               _vte_debug_print(VTE_DEBUG_EVENTS,
-                               "Keypress, modifiers=0x%x, "
-                               "keyval=0x%x\n",
-                                 m_modifiers,
-                                 keyval);
-
                /* We steal many keypad keys here. */
                if (!m_im_preedit_active) {
                        switch (keyval) {
@@ -4709,6 +4774,7 @@ Terminal::widget_key_press(vte::platform::KeyEvent const& event)
 
        /* Let the input method at this one first. */
        if (!steal && m_input_enabled) {
+                // FIXMEchpe FIXMEgtk4: update IM position? im_set_cursor_location()
                 if (m_real_widget->im_filter_keypress(event)) {
                        _vte_debug_print(VTE_DEBUG_EVENTS,
                                        "Keypress taken by IM.\n");
@@ -5040,6 +5106,16 @@ Terminal::widget_key_press(vte::platform::KeyEvent const& event)
                }
                return true;
        }
+
+#if VTE_GTK == 4
+        if (!handled &&
+            event.matches(GDK_KEY_Menu, 0)) {
+                _vte_debug_print(VTE_DEBUG_EVENTS, "Showing context menu\n");
+                // FIXMEgtk4 do context menu
+                handled = true;
+        }
+#endif
+
        return false;
 }
 
@@ -5960,6 +6036,8 @@ Terminal::invalidate_match_span()
 void
 Terminal::match_hilite_update()
 {
+       _vte_debug_print(VTE_DEBUG_EVENTS, "Match hilite update\n");
+
         /* Need to ensure the ringview is updated. */
         ringview_update();
 
@@ -5973,10 +6051,6 @@ Terminal::match_hilite_update()
         vte::base::BidiRow const* bidirow = m_ringview.get_bidirow(confine_grid_row(row));
         col = bidirow->vis2log(col);
 
-       _vte_debug_print(VTE_DEBUG_EVENTS,
-                         "Match hilite update (%ld, %ld) -> %ld, %ld\n",
-                         pos.x, pos.y, col, row);
-
         /* Whether there's any chance we'd highlight something */
         bool do_check_hilite = view_coords_visible(pos) &&
                                m_mouse_cursor_over_widget &&
@@ -6768,8 +6842,7 @@ Terminal::widget_mouse_motion(vte::platform::MouseEvent const& event)
         auto rowcol = grid_coords_from_view_coords(pos);
 
        _vte_debug_print(VTE_DEBUG_EVENTS,
-                         "Motion notify %s %s\n",
-                         pos.to_string(), rowcol.to_string());
+                         "Motion grid %s\n", rowcol.to_string());
 
         m_modifiers = event.modifiers();
 
@@ -6824,17 +6897,22 @@ Terminal::widget_mouse_press(vte::platform::MouseEvent const& event)
         /* Need to ensure the ringview is updated. */
         ringview_update();
 
+        /* Reset IM (like GtkTextView does) here */
+        if (event.press_count() == 1)
+                widget()->im_reset();
+
         auto pos = view_coords_from_event(event);
         auto rowcol = grid_coords_from_view_coords(pos);
 
+        _vte_debug_print(VTE_DEBUG_EVENTS,
+                         "Click gesture pressed button=%d at grid %s\n",
+                         event.button_value(),
+                         rowcol.to_string());
+
         m_modifiers = event.modifiers();
 
         switch (event.press_count()) {
         case 1: /* single click */
-               _vte_debug_print(VTE_DEBUG_EVENTS,
-                                 "Button %d single-click at %s\n",
-                                 event.button_value(),
-                                 rowcol.to_string());
                /* Handle this event ourselves. */
                 switch (event.button()) {
                 case vte::platform::MouseEvent::Button::eLEFT:
@@ -6844,6 +6922,8 @@ Terminal::widget_mouse_press(vte::platform::MouseEvent const& event)
                        if (!m_has_focus)
                                 widget()->grab_focus();
 
+                        // FIXMEchpe FIXMEgtk do im_reset() here
+
                        /* If we're in event mode, and the user held down the
                         * shift key, we start selecting. */
                        if (m_mouse_tracking_mode != MouseTrackingMode::eNONE) {
@@ -6904,10 +6984,6 @@ Terminal::widget_mouse_press(vte::platform::MouseEvent const& event)
                }
                break;
         case 2: /* double click */
-               _vte_debug_print(VTE_DEBUG_EVENTS,
-                                 "Button %d double-click at %s\n",
-                                 event.button_value(),
-                                 rowcol.to_string());
                 switch (event.button()) {
                 case vte::platform::MouseEvent::Button::eLEFT:
                         if (m_will_select_after_threshold) {
@@ -6928,10 +7004,6 @@ Terminal::widget_mouse_press(vte::platform::MouseEvent const& event)
                }
                break;
         case 3: /* triple click */
-               _vte_debug_print(VTE_DEBUG_EVENTS,
-                                 "Button %d triple-click at %s\n",
-                                 event.button_value(),
-                                 rowcol.to_string());
                 switch (event.button()) {
                 case vte::platform::MouseEvent::Button::eLEFT:
                         if ((m_mouse_handled_buttons & 1) != 0) {
@@ -6949,6 +7021,16 @@ Terminal::widget_mouse_press(vte::platform::MouseEvent const& event)
                break;
        }
 
+#if VTE_GTK == 4
+        if (!handled &&
+            ((event.button() == vte::platform::MouseEvent::Button::eRIGHT) ||
+             !(event.modifiers() & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK)))) {
+                _vte_debug_print(VTE_DEBUG_EVENTS, "Showing context menu\n");
+                // FIXMEgtk4 context menu
+                handled = true;
+        }
+#endif /* VTE_GTK == 4 */
+
        /* Save the pointer state for later use. */
         if (event.button_value() >= 1 && event.button_value() <= 3)
                 m_mouse_pressed_buttons |= (1 << (event.button_value() - 1));
@@ -6973,15 +7055,16 @@ Terminal::widget_mouse_release(vte::platform::MouseEvent const& event)
         auto pos = view_coords_from_event(event);
         auto rowcol = grid_coords_from_view_coords(pos);
 
+        _vte_debug_print(VTE_DEBUG_EVENTS,
+                         "Click gesture released button=%d at grid %s\n",
+                         event.button_value(), rowcol.to_string());
+
        stop_autoscroll();
 
         m_modifiers = event.modifiers();
 
         switch (event.type()) {
         case vte::platform::EventBase::Type::eMOUSE_RELEASE:
-               _vte_debug_print(VTE_DEBUG_EVENTS,
-                                 "Button %d released at %s\n",
-                                 event.button_value(), rowcol.to_string());
                 switch (event.button()) {
                 case vte::platform::MouseEvent::Button::eLEFT:
                         if ((m_mouse_handled_buttons & 1) != 0)
@@ -7020,8 +7103,6 @@ Terminal::widget_mouse_release(vte::platform::MouseEvent const& event)
 void
 Terminal::widget_focus_in()
 {
-       _vte_debug_print(VTE_DEBUG_EVENTS, "Focus in.\n");
-
         m_has_focus = true;
         widget()->grab_focus();
 
@@ -7049,8 +7130,6 @@ Terminal::widget_focus_in()
 void
 Terminal::widget_focus_out()
 {
-       _vte_debug_print(VTE_DEBUG_EVENTS, "Focus out.\n");
-
        /* We only have an IM context when we're realized, and there's not much
         * point to painting ourselves if we don't have a window. */
        if (widget_realized()) {
@@ -7083,8 +7162,9 @@ Terminal::widget_mouse_enter(vte::platform::MouseEvent const& event)
         auto pos = view_coords_from_event(event);
 
         // FIXMEchpe read event modifiers here
+        // FIXMEgtk4 or maybe not since there is no event to read them from on gtk4
 
-       _vte_debug_print(VTE_DEBUG_EVENTS, "Enter at %s\n", pos.to_string());
+       _vte_debug_print(VTE_DEBUG_EVENTS, "Motion enter at grid %s\n", pos.to_string());
 
         m_mouse_cursor_over_widget = TRUE;
         m_mouse_last_position = pos;
@@ -7098,14 +7178,23 @@ Terminal::widget_mouse_enter(vte::platform::MouseEvent const& event)
 void
 Terminal::widget_mouse_leave(vte::platform::MouseEvent const& event)
 {
+#if VTE_GTK == 3
         auto pos = view_coords_from_event(event);
 
         // FIXMEchpe read event modifiers here
+        // FIXMEgtk4 or maybe not since there is no event to read them from on gtk4
 
-       _vte_debug_print(VTE_DEBUG_EVENTS, "Leave at %s\n", pos.to_string());
+       _vte_debug_print(VTE_DEBUG_EVENTS, "Motion leave at grid %s\n", pos.to_string());
 
         m_mouse_cursor_over_widget = FALSE;
         m_mouse_last_position = pos;
+#elif VTE_GTK == 4
+        // FIXMEgtk4 !!!
+        m_mouse_cursor_over_widget = false;
+        // keep m_mouse_last_position since the event here has no position
+#endif
+
+        // FIXMEchpe: also set m_mouse_scroll_delta to 0 here?
 
         hyperlink_hilite_update();
         match_hilite_update();
@@ -7193,7 +7282,11 @@ Terminal::apply_font_metrics(int cell_width_unscaled,
        /* Queue a resize if anything's changed. */
        if (resize) {
                if (widget_realized()) {
+#if VTE_GTK == 3
                        gtk_widget_queue_resize_no_redraw(m_widget);
+#elif VTE_GTK == 4
+                        gtk_widget_queue_resize(m_widget); // FIXMEgtk4?
+#endif
                }
        }
        /* Emit a signal that the font changed. */
@@ -7306,6 +7399,7 @@ Terminal::set_font_desc(vte::Freeable<PangoFontDescription> font_desc)
 bool
 Terminal::update_font_desc()
 {
+#if VTE_GTK == 3
         auto desc = vte::Freeable<PangoFontDescription>{};
 
         auto context = gtk_widget_get_style_context(m_widget);
@@ -7315,6 +7409,16 @@ Terminal::update_font_desc()
                               &vte::get_freeable(desc),
                               nullptr);
         gtk_style_context_restore(context);
+#elif VTE_GTK == 4
+        // FIXMEgtk4
+        // This is how gtktextview does it, but the APIs are private... thanks, gtk4!
+        // desc = vte::take_freeable
+        //          (gtk_css_style_get_pango_font(gtk_style_context_lookup_style(context));
+
+        auto context = gtk_widget_get_pango_context(m_widget);
+        auto context_desc = pango_context_get_font_description(context);
+        auto desc = vte::take_freeable(pango_font_description_copy(context_desc));
+#endif /* VTE_GTK */
 
        pango_font_description_set_family_static(desc.get(), "monospace");
 
@@ -7633,7 +7737,11 @@ Terminal::set_size(long columns,
                                                    _vte_ring_next (m_screen->row_data) - 1));
 
                adjust_adjustments_full();
+#if VTE_GTK == 3
                gtk_widget_queue_resize_no_redraw(m_widget);
+#elif VTE_GTK == 4
+                gtk_widget_queue_resize(m_widget); // FIXMEgtk4?
+#endif
                /* Our visible text changed. */
                emit_text_modified();
        }
@@ -7798,11 +7906,9 @@ Terminal::Terminal(vte::platform::Widget* w,
 }
 
 void
-Terminal::widget_get_preferred_width(int *minimum_width,
-                                               int *natural_width)
+Terminal::widget_measure_width(int *minimum_width,
+                               int *natural_width)
 {
-       _vte_debug_print(VTE_DEBUG_LIFECYCLE, "vte_terminal_get_preferred_width()\n");
-
        ensure_font();
 
         refresh_size();
@@ -7825,11 +7931,9 @@ Terminal::widget_get_preferred_width(int *minimum_width,
 }
 
 void
-Terminal::widget_get_preferred_height(int *minimum_height,
-                                                int *natural_height)
+Terminal::widget_measure_height(int *minimum_height,
+                                int *natural_height)
 {
-       _vte_debug_print(VTE_DEBUG_LIFECYCLE, "vte_terminal_get_preferred_height()\n");
-
        ensure_font();
 
         refresh_size();
@@ -7852,7 +7956,17 @@ Terminal::widget_get_preferred_height(int *minimum_height,
 }
 
 void
-Terminal::widget_size_allocate(GtkAllocation *allocation)
+#if VTE_GTK == 3
+Terminal::widget_size_allocate(int allocation_x,
+                               int allocation_y,
+                               int allocation_width,
+                               int allocation_height,
+                               int allocation_baseline)
+#elif VTE_GTK == 4
+Terminal::widget_size_allocate(int allocation_width,
+                               int allocation_height,
+                               int allocation_baseline)
+#endif /* VTE_GTK */
 {
        glong width, height;
        gboolean repaint, update_scrollback;
@@ -7860,9 +7974,9 @@ Terminal::widget_size_allocate(GtkAllocation *allocation)
        _vte_debug_print(VTE_DEBUG_LIFECYCLE,
                        "vte_terminal_size_allocate()\n");
 
-       width = (allocation->width - (m_padding.left + m_padding.right)) /
+       width = (allocation_width - (m_padding.left + m_padding.right)) /
                m_cell_width;
-       height = (allocation->height - (m_padding.top + m_padding.bottom)) /
+       height = (allocation_height - (m_padding.top + m_padding.bottom)) /
                 m_cell_height;
        width = MAX(width, 1);
        height = MAX(height, 1);
@@ -7870,19 +7984,21 @@ Terminal::widget_size_allocate(GtkAllocation *allocation)
        _vte_debug_print(VTE_DEBUG_WIDGET_SIZE,
                        "[Terminal %p] Sizing window to %dx%d (%ldx%ld, padding %d,%d;%d,%d).\n",
                         m_terminal,
-                       allocation->width, allocation->height,
+                       allocation_width, allocation_height,
                          width, height,
                          m_padding.left, m_padding.right, m_padding.top, m_padding.bottom);
 
         auto current_allocation = get_allocated_rect();
 
-       repaint = current_allocation.width != allocation->width
-                       || current_allocation.height != allocation->height;
-       update_scrollback = current_allocation.height != allocation->height;
+       repaint = current_allocation.width != allocation_width ||
+                current_allocation.height != allocation_height;
+       update_scrollback = current_allocation.height != allocation_height;
 
-       /* Set our allocation to match the structure. */
-       gtk_widget_set_allocation(m_widget, allocation);
-        set_allocated_rect(*allocation);
+#if VTE_GTK == 3
+        set_allocated_rect({allocation_x, allocation_y, allocation_width, allocation_height});
+#elif VTE_GTK == 4
+        set_allocated_rect({0, 0, allocation_width, allocation_height});
+#endif
 
        if (width != m_column_count
                        || height != m_row_count
@@ -7949,8 +8065,9 @@ Terminal::set_blink_settings(bool blink,
                              int blink_time,
                              int blink_timeout) noexcept
 {
-        m_cursor_blink_cycle = blink_time / 2;
-        m_cursor_blink_timeout = blink_timeout;
+        m_cursor_blinks = m_cursor_blinks_system = blink;
+        m_cursor_blink_cycle = std::max(blink_time / 2, VTE_MIN_CURSOR_BLINK_CYCLE);
+        m_cursor_blink_timeout = std::max(blink_timeout, VTE_MIN_CURSOR_BLINK_TIMEOUT);
 
         update_cursor_blinks();
 
@@ -9222,6 +9339,7 @@ Terminal::paint_im_preedit_string()
                                         height,
                                         get_color(VTE_DEFAULT_BG), m_background_alpha);
                 }
+
                draw_cells_with_attributes(
                                                        items, len,
                                                        m_im_preedit_attrs.get(),
@@ -9248,11 +9366,41 @@ Terminal::paint_im_preedit_string()
        }
 }
 
+#if VTE_GTK == 3
+
+void
+Terminal::widget_draw(cairo_t* cr)
+{
+#ifdef VTE_DEBUG
+        _VTE_DEBUG_IF(VTE_DEBUG_LIFECYCLE | VTE_DEBUG_WORK | VTE_DEBUG_UPDATES) do {
+                auto clip_rect = cairo_rectangle_int_t{};
+                if (!gdk_cairo_get_clip_rectangle (cr, &clip_rect))
+                        break;
+
+                _vte_debug_print(VTE_DEBUG_LIFECYCLE, "vte_terminal_draw()\n");
+                _vte_debug_print (VTE_DEBUG_WORK, "+");
+                _vte_debug_print (VTE_DEBUG_UPDATES, "Draw (%d,%d)x(%d,%d)\n",
+                                  clip_rect.x, clip_rect.y,
+                                  clip_rect.width, clip_rect.height);
+        } while (0);
+#endif /* VTE_DEBUG */
+
+        auto region = vte_cairo_get_clip_region(cr);
+        if (!region)
+                return;
+
+        /* Transform to view coordinates */
+        cairo_region_translate(region.get(), -m_padding.left, -m_padding.top);
+
+        draw(cr, region.get());
+}
+
+#endif /* VTE_GTK == 3 */
+
 void
-Terminal::widget_draw(cairo_t *cr)
+Terminal::draw(cairo_t* cr,
+               cairo_region_t const* region)
 {
-        cairo_rectangle_int_t clip_rect;
-        cairo_region_t *region;
         int allocated_width, allocated_height;
         int extra_area_for_cursor;
         bool text_blink_enabled_now;
@@ -9261,19 +9409,6 @@ Terminal::widget_draw(cairo_t *cr)
 #endif
         gint64 now = 0;
 
-        if (!gdk_cairo_get_clip_rectangle (cr, &clip_rect))
-                return;
-
-        _vte_debug_print(VTE_DEBUG_LIFECYCLE, "vte_terminal_draw()\n");
-        _vte_debug_print (VTE_DEBUG_WORK, "+");
-        _vte_debug_print (VTE_DEBUG_UPDATES, "Draw (%d,%d)x(%d,%d)\n",
-                          clip_rect.x, clip_rect.y,
-                          clip_rect.width, clip_rect.height);
-
-        region = vte_cairo_get_clip_region (cr);
-        if (region == NULL)
-                return;
-
         allocated_width = get_allocated_width();
         allocated_height = get_allocated_height();
 
@@ -9294,9 +9429,6 @@ Terminal::widget_draw(cairo_t *cr)
 
         cairo_translate(cr, m_padding.left, m_padding.top);
 
-        /* Transform to view coordinates */
-        cairo_region_translate(region, -m_padding.left, -m_padding.top);
-
 #ifdef WITH_SIXEL
        /* Draw images */
        if (m_images_enabled) {
@@ -9365,8 +9497,6 @@ Terminal::widget_draw(cairo_t *cr)
        /* Done with various structures. */
        m_draw.set_cairo(nullptr);
 
-        cairo_region_destroy (region);
-
         /* If painting encountered any cell with blink attribute, we might need to set up a timer.
          * Blinking is implemented using a one-shot (not repeating) timer that keeps getting reinstalled
          * here as long as blinking cells are encountered during (re)painting. This way there's no need
@@ -9380,47 +9510,43 @@ Terminal::widget_draw(cairo_t *cr)
         m_invalidated_all = FALSE;
 }
 
+#if VTE_GTK == 3
+
 /* Handle an expose event by painting the exposed area. */
-static cairo_region_t *
-vte_cairo_get_clip_region (cairo_t *cr)
+static vte::Freeable<cairo_region_t>
+vte_cairo_get_clip_region(cairo_t *cr)
 {
-        cairo_rectangle_list_t *list;
-        cairo_region_t *region;
-        int i;
-
-        list = cairo_copy_clip_rectangle_list (cr);
+        auto list = vte::take_freeable(cairo_copy_clip_rectangle_list(cr));
         if (list->status == CAIRO_STATUS_CLIP_NOT_REPRESENTABLE) {
-                cairo_rectangle_int_t clip_rect;
 
-                cairo_rectangle_list_destroy (list);
+                auto clip_rect = cairo_rectangle_int_t{};
+                if (!gdk_cairo_get_clip_rectangle(cr, &clip_rect))
+                        return nullptr;
 
-                if (!gdk_cairo_get_clip_rectangle (cr, &clip_rect))
-                        return NULL;
-                return cairo_region_create_rectangle (&clip_rect);
+                return vte::take_freeable(cairo_region_create_rectangle(&clip_rect));
         }
 
+        auto region = vte::take_freeable(cairo_region_create());
+        for (auto i = list->num_rectangles - 1; i >= 0; --i) {
+                auto rect = &list->rectangles[i];
 
-        region = cairo_region_create ();
-        for (i = list->num_rectangles - 1; i >= 0; --i) {
-                cairo_rectangle_t *rect = &list->rectangles[i];
                 cairo_rectangle_int_t clip_rect;
-
                 clip_rect.x = floor (rect->x);
                 clip_rect.y = floor (rect->y);
                 clip_rect.width = ceil (rect->x + rect->width) - clip_rect.x;
                 clip_rect.height = ceil (rect->y + rect->height) - clip_rect.y;
 
-                if (cairo_region_union_rectangle (region, &clip_rect) != CAIRO_STATUS_SUCCESS) {
-                        cairo_region_destroy (region);
-                        region = NULL;
+                if (cairo_region_union_rectangle(region.get(), &clip_rect) != CAIRO_STATUS_SUCCESS) {
+                        region.reset();
                         break;
                 }
         }
 
-        cairo_rectangle_list_destroy (list);
         return region;
 }
 
+#endif /* VTE_GTK == 3 */
+
 bool
 Terminal::widget_mouse_scroll(vte::platform::ScrollEvent const& event)
 {
@@ -9652,15 +9778,11 @@ Terminal::set_rewrap_on_resize(bool rewrap)
 void
 Terminal::update_cursor_blinks()
 {
-        bool blink = false;
+        auto blink = false;
 
         switch (decscusr_cursor_blink()) {
         case CursorBlinkMode::eSYSTEM:
-                gboolean v;
-                g_object_get(gtk_widget_get_settings(m_widget),
-                                                     "gtk-cursor-blink",
-                                                     &v, nullptr);
-                blink = v != FALSE;
+                blink = m_cursor_blinks_system;
                 break;
         case CursorBlinkMode::eON:
                 blink = true;
@@ -10507,6 +10629,7 @@ Terminal::invalidate_dirty_rects_and_process_updates()
        if (G_UNLIKELY (!m_update_rects->len))
                return false;
 
+#if VTE_GTK == 3
         auto region = cairo_region_create();
         auto n_rects = m_update_rects->len;
         for (guint i = 0; i < n_rects; i++) {
@@ -10524,6 +10647,9 @@ Terminal::invalidate_dirty_rects_and_process_updates()
        /* and perform the merge with the window visible area */
         gtk_widget_queue_draw_region(m_widget, region);
        cairo_region_destroy (region);
+#elif VTE_GTK == 4
+        gtk_widget_queue_draw(m_widget); // FIXMEgtk4
+#endif
 
        return true;
 }
@@ -10928,7 +11054,7 @@ Terminal::set_input_enabled (bool enabled)
                 if (m_has_focus)
                         widget()->im_focus_in();
 
-                gtk_style_context_remove_class (context, GTK_STYLE_CLASS_READ_ONLY);
+                gtk_style_context_remove_class (context, VTE_STYLE_CLASS_READ_ONLY);
         } else {
                 im_reset();
                 if (m_has_focus)
@@ -10937,7 +11063,7 @@ Terminal::set_input_enabled (bool enabled)
                 disconnect_pty_write();
                 _vte_byte_array_clear(m_outgoing);
 
-                gtk_style_context_add_class (context, GTK_STYLE_CLASS_READ_ONLY);
+                gtk_style_context_add_class (context, VTE_STYLE_CLASS_READ_ONLY);
         }
 
         return true;
diff --git a/src/vte/meson.build b/src/vte/meson.build
index 6d672a49..04b6fa2f 100644
--- a/src/vte/meson.build
+++ b/src/vte/meson.build
@@ -16,21 +16,6 @@
 
 vte_inc = include_directories('.')
 
-libvte_common_enum_headers = files(
-  # These files contain enums to be extracted by glib-mkenums
-  'vtedeprecated.h',
-  'vteenums.h',
-)
-
-libvte_gtk3_enum_sources = gnome.mkenums(
-  'vtetypebuiltins.h',
-  sources: libvte_common_enum_headers,
-  c_template: '../vtetypebuiltins.cc.template',
-  h_template: '../vtetypebuiltins.h.template',
-  install_header: true,
-  install_dir: vte_includedir / vte_gtk3_api_path
-)
-
 libvte_common_public_headers = files(
   'vte.h',
   'vtedeprecated.h',
@@ -40,31 +25,111 @@ libvte_common_public_headers = files(
   'vtepty.h',
   'vteregex.h',
   'vteterminal.h',
+  'vtetypebuiltins.h',
 )
 
+libvte_common_enum_headers = files(
+  # These files contain enums to be extracted by glib-mkenums
+  'vtedeprecated.h',
+  'vteenums.h',
+)
+
+# Version header
+
 vteversion_conf = configuration_data()
 vteversion_conf.set('VTE_MAJOR_VERSION', vte_major_version)
 vteversion_conf.set('VTE_MINOR_VERSION', vte_minor_version)
 vteversion_conf.set('VTE_MICRO_VERSION', vte_micro_version)
 
-libvte_version_headers = configure_file(
+libvte_common_public_headers += configure_file(
   input: 'vteversion.h.in',
   output: '@BASENAME@',
   configuration: vteversion_conf,
   install: false,
 )
 
+# Install headers, and create the type builtin files.
+# Note that we cannot use gnome.mkenums() to create the type builtins
+# files, since we need to install the generated header for both gtk3
+# and gtk4, and gnome.mkenums does not work with install_header()
+# [https://github.com/mesonbuild/meson/issues/1687]. However, neither does
+# custom_target() itself.
+# So we need to generate differently-named files for gtk3 and gtk4, and
+# install them sepearately, with an extra header that includes the right
+# one. And since gnome.mkenums() does not allow specifying the output names
+# when using templates, we need to use custom_target() for that.
+glib_mkenums = find_program('glib-mkenums')
+
 if get_option('gtk3')
-  libvte_gtk3_public_headers = libvte_common_public_headers + [libvte_version_headers]
+
+  libvte_gtk3_public_headers = libvte_common_public_headers
 
   install_headers(
     libvte_gtk3_public_headers,
     subdir: vte_gtk3_api_path
   )
 
-  # BUG! Due to meson bug, this header cannot be installed with the rule above. Instead,
-  # use the install_header attribute in the mkenums call, and add the header afterwards
-  # to the list.
-  libvte_gtk3_public_headers += libvte_gtk3_enum_sources[1]
+  libvte_gtk3_public_headers += custom_target(
+    'vtetypebuiltins-gtk3.h',
+    command: [
+      glib_mkenums,
+      '--output', '@OUTPUT@',
+      '--template', meson.current_source_dir() / '..' / 'vtetypebuiltins.h.template',
+      '@INPUT@',
+    ],
+    input: libvte_common_enum_headers,
+    install: true,
+    install_dir: vte_includedir / vte_gtk3_api_path,
+    output: 'vtetypebuiltins-gtk3.h',
+  )
+
+  libvte_gtk3_enum_sources = [custom_target(
+    'vtetypebuiltins-gtk3.cc',
+    command: [
+      glib_mkenums,
+      '--output', '@OUTPUT@',
+      '--template', meson.current_source_dir() / '..' / 'vtetypebuiltins.cc.template',
+      '@INPUT@',
+    ],
+    input: libvte_common_enum_headers,
+    install: false,
+    output: 'vtetypebuiltins-gtk3.cc',
+  ),]
+endif
+
+if get_option('gtk4')
+
+  libvte_gtk4_public_headers = libvte_common_public_headers
+
+  install_headers(
+    libvte_gtk4_public_headers,
+    subdir: vte_gtk4_api_path
+  )
+
+  libvte_gtk4_public_headers += custom_target(
+    'vtetypebuiltins-gtk4.h',
+    command: [
+      glib_mkenums,
+      '--output', '@OUTPUT@',
+      '--template', meson.current_source_dir() / '..' / 'vtetypebuiltins.h.template',
+      '@INPUT@',
+    ],
+    input: libvte_common_enum_headers,
+    install: true,
+    install_dir: vte_includedir / vte_gtk4_api_path,
+    output: 'vtetypebuiltins-gtk4.h',
+  )
 
+  libvte_gtk4_enum_sources = [custom_target(
+    'vtetypebuiltins-gtk4.cc',
+    command: [
+      glib_mkenums,
+      '--output', '@OUTPUT@',
+      '--template', meson.current_source_dir() / '..' / 'vtetypebuiltins.cc.template',
+      '@INPUT@',
+    ],
+    input: libvte_common_enum_headers,
+    install: false,
+    output: 'vtetypebuiltins-gtk4.cc',
+  ),]
 endif
diff --git a/src/vte/vte.h b/src/vte/vte.h
index d4a6ff58..c9fa1fe6 100644
--- a/src/vte/vte.h
+++ b/src/vte/vte.h
@@ -18,9 +18,13 @@
 #pragma once
 
 #include <glib.h>
+#include <gtk/gtk.h>
 
 #define __VTE_VTE_H_INSIDE__ 1
 
+/* This must always be included first */
+#include "vtemacros.h"
+
 #include "vteenums.h"
 #include "vteglobals.h"
 #include "vtepty.h"
diff --git a/src/vte/vtedeprecated.h b/src/vte/vtedeprecated.h
index cbc1b57c..fd4913eb 100644
--- a/src/vte/vtedeprecated.h
+++ b/src/vte/vtedeprecated.h
@@ -33,11 +33,15 @@
 
 G_BEGIN_DECLS
 
+#if _VTE_GTK == 3
+
 _VTE_DEPRECATED
 _VTE_PUBLIC
 int vte_terminal_match_add_gregex(VteTerminal *terminal,
                                   GRegex *gregex,
-                                  GRegexMatchFlags gflags) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) 
_VTE_GNUC_NONNULL(2);
+                                  GRegexMatchFlags gflags) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 2);
+
+#endif /* _VTE_GTK == 3 */
 
 _VTE_DEPRECATED
 _VTE_PUBLIC
@@ -45,11 +49,13 @@ void vte_terminal_match_set_cursor(VteTerminal *terminal,
                                    int tag,
                                    GdkCursor *cursor) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1);
 
+#if _VTE_GTK == 3
 _VTE_DEPRECATED
 _VTE_PUBLIC
 void vte_terminal_match_set_cursor_type(VteTerminal *terminal,
                                        int tag,
                                         GdkCursorType cursor_type) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1);
+#endif
 
 _VTE_DEPRECATED
 _VTE_PUBLIC
@@ -57,6 +63,8 @@ char *vte_terminal_match_check(VteTerminal *terminal,
                               glong column, glong row,
                               int *tag) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) G_GNUC_MALLOC;
 
+#if _VTE_GTK == 3
+
 _VTE_DEPRECATED
 _VTE_PUBLIC
 gboolean vte_terminal_event_check_gregex_simple(VteTerminal *terminal,
@@ -64,7 +72,7 @@ gboolean vte_terminal_event_check_gregex_simple(VteTerminal *terminal,
                                                 GRegex **regexes,
                                                 gsize n_regexes,
                                                 GRegexMatchFlags match_flags,
-                                                char **matches) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) 
_VTE_GNUC_NONNULL(2);
+                                                char **matches) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 2);
 
 _VTE_DEPRECATED
 _VTE_PUBLIC
@@ -76,6 +84,8 @@ _VTE_DEPRECATED
 _VTE_PUBLIC
 GRegex   *vte_terminal_search_get_gregex      (VteTerminal *terminal) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1);
 
+#endif /* _VTE_GTK == 3 */
+
 _VTE_DEPRECATED
 _VTE_PUBLIC
 gboolean vte_terminal_spawn_sync(VteTerminal *terminal,
@@ -88,7 +98,7 @@ gboolean vte_terminal_spawn_sync(VteTerminal *terminal,
                                  gpointer child_setup_data,
                                  GPid *child_pid /* out */,
                                  GCancellable *cancellable,
-                                 GError **error) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) _VTE_GNUC_NONNULL(4);
+                                 GError **error) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 4);
 
 _VTE_DEPRECATED
 _VTE_PUBLIC
@@ -98,17 +108,21 @@ _VTE_DEPRECATED
 _VTE_PUBLIC
 void vte_terminal_copy_clipboard(VteTerminal *terminal) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1);
 
+#if _VTE_GTK == 3
+
 _VTE_DEPRECATED
 _VTE_PUBLIC
 void vte_terminal_get_geometry_hints(VteTerminal *terminal,
                                      GdkGeometry *hints,
                                      int min_rows,
-                                     int min_columns) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) 
_VTE_GNUC_NONNULL(2);
+                                     int min_columns) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 2);
 
 _VTE_DEPRECATED
 _VTE_PUBLIC
 void vte_terminal_set_geometry_hints_for_window(VteTerminal *terminal,
-                                                GtkWindow *window) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) 
_VTE_GNUC_NONNULL(2);
+                                                GtkWindow *window) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 2);
+
+#endif /* _VTE_GTK == 3 */
 
 _VTE_DEPRECATED
 _VTE_PUBLIC
diff --git a/src/vte/vtemacros.h b/src/vte/vtemacros.h
index 30ba7039..c1300998 100644
--- a/src/vte/vtemacros.h
+++ b/src/vte/vtemacros.h
@@ -21,6 +21,18 @@
 #error "Only <vte/vte.h> can be included directly."
 #endif
 
+#include <gtk/gtk.h>
+
+#if GTK_CHECK_VERSION(4,0,0)
+#define _VTE_GTK 4
+#elif GTK_CHECK_VERSION(3,90,0)
+#error gtk+ version not supported
+#elif GTK_CHECK_VERSION(3,0,0)
+#define _VTE_GTK 3
+#else
+#error gtk+ version unknown
+#endif
+
 #if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ > 6)
 #define _VTE_GNUC_PACKED __attribute__((__packed__))
 #else
@@ -28,12 +40,12 @@
 #endif  /* !__GNUC__ */
 
 #ifdef VTE_COMPILATION
-#define _VTE_GNUC_NONNULL(position)
+#define _VTE_GNUC_NONNULL(...)
 #else
-#if __GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ > 2)
-#define _VTE_GNUC_NONNULL(position) __attribute__((__nonnull__(position)))
+#if defined(__GNUC__)
+#define _VTE_GNUC_NONNULL(...) __attribute__((__nonnull__(__VA_ARGS__)))
 #else
-#define _VTE_GNUC_NONNULL(position)
+#define _VTE_GNUC_NONNULL(...)
 #endif
 #endif
 
diff --git a/src/vte/vtepty.h b/src/vte/vtepty.h
index 524e91d6..f6348cd7 100644
--- a/src/vte/vtepty.h
+++ b/src/vte/vtepty.h
@@ -91,8 +91,6 @@ gboolean vte_pty_set_utf8 (VtePty *pty,
                            gboolean utf8,
                            GError **error) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1);
 
-G_DEFINE_AUTOPTR_CLEANUP_FUNC(VtePty, g_object_unref)
-
 _VTE_PUBLIC
 void vte_pty_spawn_async(VtePty *pty,
                          const char *working_directory,
@@ -105,7 +103,7 @@ void vte_pty_spawn_async(VtePty *pty,
                          int timeout,
                          GCancellable *cancellable,
                          GAsyncReadyCallback callback,
-                         gpointer user_data) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) _VTE_GNUC_NONNULL(3);
+                         gpointer user_data) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 3);
 
 _VTE_PUBLIC
 void vte_pty_spawn_with_fds_async(VtePty *pty,
@@ -123,12 +121,14 @@ void vte_pty_spawn_with_fds_async(VtePty *pty,
                                   int timeout,
                                   GCancellable *cancellable,
                                   GAsyncReadyCallback callback,
-                                  gpointer user_data) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) 
_VTE_GNUC_NONNULL(3);
+                                  gpointer user_data) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 3);
 
 _VTE_PUBLIC
 gboolean vte_pty_spawn_finish(VtePty *pty,
                               GAsyncResult *result,
                               GPid *child_pid /* out */,
-                              GError **error) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) _VTE_GNUC_NONNULL(2);
+                              GError **error) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 2);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(VtePty, g_object_unref)
 
 G_END_DECLS
diff --git a/src/vte/vteregex.h b/src/vte/vteregex.h
index 626ae87c..6eb95343 100644
--- a/src/vte/vteregex.h
+++ b/src/vte/vteregex.h
@@ -71,7 +71,7 @@ char *vte_regex_substitute(VteRegex *regex,
                            const char *subject,
                            const char *replacement,
                            guint32 flags,
-                           GError **error) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) _VTE_GNUC_NONNULL(2) 
_VTE_GNUC_NONNULL(3) G_GNUC_MALLOC;
+                           GError **error) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 2, 3) G_GNUC_MALLOC;
 
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(VteRegex, vte_regex_unref)
 
diff --git a/src/vte/vteterminal.h b/src/vte/vteterminal.h
index e705ece6..736b3e77 100644
--- a/src/vte/vteterminal.h
+++ b/src/vte/vteterminal.h
@@ -50,8 +50,10 @@ typedef struct _VteCharAttributes       VteCharAttributes;
  */
 struct _VteTerminal {
        GtkWidget widget;
+#if _VTE_GTK == 3
         /*< private >*/
        gpointer *_unused_padding[1]; /* FIXMEchpe: remove this field on the next ABI break */
+#endif
 };
 
 /**
@@ -105,6 +107,7 @@ struct _VteTerminalClass {
         /* Padding for future expansion. */
         gpointer padding[16];
 
+// FIXMEgtk4 use class private data instead
         VteTerminalClassPrivate *priv;
 };
 
@@ -156,7 +159,7 @@ void vte_terminal_spawn_async(VteTerminal *terminal,
                               int timeout,
                               GCancellable *cancellable,
                               VteTerminalSpawnAsyncCallback callback,
-                              gpointer user_data) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) 
_VTE_GNUC_NONNULL(4);
+                              gpointer user_data) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 4);
 
 _VTE_PUBLIC
 void vte_terminal_spawn_with_fds_async(VteTerminal* terminal,
@@ -175,7 +178,7 @@ void vte_terminal_spawn_with_fds_async(VteTerminal* terminal,
                                        int timeout,
                                        GCancellable* cancellable,
                                        VteTerminalSpawnAsyncCallback callback,
-                                       gpointer user_data) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) 
_VTE_GNUC_NONNULL(4);
+                                       gpointer user_data) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 4);
 
 /* Send data to the terminal to display, or to the terminal's forked command
  * to handle in some way.  If it's 'cat', they should be the same. */
@@ -268,10 +271,10 @@ void vte_terminal_set_color_bold(VteTerminal *terminal,
                                  const GdkRGBA *bold) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1);
 _VTE_PUBLIC
 void vte_terminal_set_color_foreground(VteTerminal *terminal,
-                                       const GdkRGBA *foreground) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) 
_VTE_GNUC_NONNULL(2);
+                                       const GdkRGBA *foreground) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 2);
 _VTE_PUBLIC
 void vte_terminal_set_color_background(VteTerminal *terminal,
-                                       const GdkRGBA *background) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) 
_VTE_GNUC_NONNULL(2);
+                                       const GdkRGBA *background) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 2);
 _VTE_PUBLIC
 void vte_terminal_set_color_cursor(VteTerminal *terminal,
                                    const GdkRGBA *cursor_background) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1);
@@ -395,21 +398,25 @@ void vte_terminal_get_cursor_position(VteTerminal *terminal,
                                      glong *column,
                                       glong *row) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1);
 
+#if _VTE_GTK == 3
+
 _VTE_PUBLIC
 char *vte_terminal_hyperlink_check_event(VteTerminal *terminal,
-                                         GdkEvent *event) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) 
_VTE_GNUC_NONNULL(2) G_GNUC_MALLOC;
+                                         GdkEvent *event) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 2) 
G_GNUC_MALLOC;
+
+#endif /* _VTE_GTK */
 
 /* Add a matching expression, returning the tag the widget assigns to that
  * expression. */
 _VTE_PUBLIC
 int vte_terminal_match_add_regex(VteTerminal *terminal,
                                  VteRegex *regex,
-                                 guint32 flags) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) _VTE_GNUC_NONNULL(2);
+                                 guint32 flags) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 2);
 /* Set the cursor to be used when the pointer is over a given match. */
 _VTE_PUBLIC
 void vte_terminal_match_set_cursor_name(VteTerminal *terminal,
                                        int tag,
-                                        const char *cursor_name) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) 
_VTE_GNUC_NONNULL(3);
+                                        const char *cursor_name) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 3);
 _VTE_PUBLIC
 void vte_terminal_match_remove(VteTerminal *terminal,
                                int tag) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1);
@@ -419,24 +426,29 @@ void vte_terminal_match_remove_all(VteTerminal *terminal) _VTE_CXX_NOEXCEPT _VTE
 /* Check if a given cell on the screen contains part of a matched string.  If
  * it does, return the string, and store the match tag in the optional tag
  * argument. */
+#if _VTE_GTK == 3
+
 _VTE_PUBLIC
 char *vte_terminal_match_check_event(VteTerminal *terminal,
                                      GdkEvent *event,
-                                     int *tag) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) _VTE_GNUC_NONNULL(2) 
G_GNUC_MALLOC;
+                                     int *tag) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 2) G_GNUC_MALLOC;
+
 _VTE_PUBLIC
 char **vte_terminal_event_check_regex_array(VteTerminal *terminal,
                                             GdkEvent *event,
                                             VteRegex **regexes,
                                             gsize n_regexes,
                                             guint32 match_flags,
-                                            gsize *n_matches) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) 
_VTE_GNUC_NONNULL(2) G_GNUC_MALLOC;
+                                            gsize *n_matches) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 2) 
G_GNUC_MALLOC;
 _VTE_PUBLIC
 gboolean vte_terminal_event_check_regex_simple(VteTerminal *terminal,
                                                GdkEvent *event,
                                                VteRegex **regexes,
                                                gsize n_regexes,
                                                guint32 match_flags,
-                                               char **matches) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) 
_VTE_GNUC_NONNULL(2);
+                                               char **matches) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 2);
+
+#endif /* _VTE_GTK */
 
 _VTE_PUBLIC
 void      vte_terminal_search_set_regex      (VteTerminal *terminal,
@@ -492,12 +504,17 @@ _VTE_PUBLIC
 gboolean vte_terminal_get_input_enabled (VteTerminal *terminal) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1);
 
 /* rarely useful functions */
+
+#if _VTE_GTK == 3
+
 _VTE_PUBLIC
 void vte_terminal_set_clear_background(VteTerminal* terminal,
                                        gboolean setting) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1);
 _VTE_PUBLIC
 void vte_terminal_get_color_background_for_draw(VteTerminal* terminal,
-                                                GdkRGBA* color) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) 
_VTE_GNUC_NONNULL(2);
+                                                GdkRGBA* color) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 2);
+
+#endif /* _VTE_GTK == 3 */
 
 /* Writing contents out */
 _VTE_PUBLIC
@@ -505,7 +522,7 @@ gboolean vte_terminal_write_contents_sync (VteTerminal *terminal,
                                            GOutputStream *stream,
                                            VteWriteFlags flags,
                                            GCancellable *cancellable,
-                                           GError **error) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1) 
_VTE_GNUC_NONNULL(2);
+                                           GError **error) _VTE_CXX_NOEXCEPT _VTE_GNUC_NONNULL(1, 2);
 
 /* Images */
 
diff --git a/src/vte/vtetypebuiltins.h b/src/vte/vtetypebuiltins.h
new file mode 100644
index 00000000..41334fea
--- /dev/null
+++ b/src/vte/vtetypebuiltins.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright © 2021 Christian Persch
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#if !defined (__VTE_VTE_H_INSIDE__) && !defined (VTE_COMPILATION)
+#error "Only <vte/vte.h> can be included directly."
+#endif
+
+#if _VTE_GTK == 3
+#include "vtetypebuiltins-gtk3.h"
+#elif _VTE_GTK == 4
+#include "vtetypebuiltins-gtk4.h"
+#endif
diff --git a/src/vteaccess.h b/src/vteaccess.h
index 4362ede4..31f21601 100644
--- a/src/vteaccess.h
+++ b/src/vteaccess.h
@@ -15,9 +15,7 @@
  * along with this library.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-#ifndef vte_vteaccess_h_included
-#define vte_vteaccess_h_included
-
+#pragma once
 
 #include <glib.h>
 #include <gtk/gtk.h>
@@ -51,5 +49,3 @@ struct _VteTerminalAccessibleClass {
 GType _vte_terminal_accessible_get_type(void);
 
 G_END_DECLS
-
-#endif
diff --git a/src/vtedefines.hh b/src/vtedefines.hh
index b1720735..16f6ae78 100644
--- a/src/vtedefines.hh
+++ b/src/vtedefines.hh
@@ -148,3 +148,6 @@
 #define VTE_SIXEL_MAX_WIDTH (2048)
 #define VTE_SIXEL_MAX_HEIGHT (2052)
 #define VTE_SIXEL_NUM_COLOR_REGISTERS (1024)
+
+#define VTE_MIN_CURSOR_BLINK_CYCLE (50 /* ms */)
+#define VTE_MIN_CURSOR_BLINK_TIMEOUT (50 /* ms */)
diff --git a/src/vtegtk.cc b/src/vtegtk.cc
index a1be90df..24623405 100644
--- a/src/vtegtk.cc
+++ b/src/vtegtk.cc
@@ -63,8 +63,12 @@
 #include "vteregexinternal.hh"
 
 #ifdef WITH_A11Y
+#if VTE_GTK == 3
 #include "vteaccess.h"
-#endif
+#else
+#undef WITH_A11Y
+#endif /* VTE_GTK == 3 */
+#endif /* WITH_A11Y */
 
 #ifdef WITH_ICU
 #include "icu-glue.hh"
@@ -79,6 +83,19 @@ struct _VteTerminalClassPrivate {
         GtkStyleProvider *style_provider;
 };
 
+#if VTE_GTK == 4
+
+static void
+style_provider_parsing_error_cb(GtkCssProvider* provider,
+                                void* section,
+                                GError* error)
+{
+        g_assert_no_error(error);
+}
+
+#endif
+
+
 class VteTerminalPrivate {
 public:
         VteTerminalPrivate(VteTerminal* terminal)
@@ -217,7 +234,6 @@ vte_terminal_set_hscroll_policy(VteTerminal *terminal,
 try
 {
         WIDGET(terminal)->set_hscroll_policy(policy);
-        gtk_widget_queue_resize_no_redraw (GTK_WIDGET (terminal));
 }
 catch (...)
 {
@@ -230,7 +246,6 @@ vte_terminal_set_vscroll_policy(VteTerminal *terminal,
 try
 {
         WIDGET(terminal)->set_vscroll_policy(policy);
-        gtk_widget_queue_resize_no_redraw (GTK_WIDGET (terminal));
 }
 catch (...)
 {
@@ -260,6 +275,8 @@ catch (...)
         vte::log_exception();
 }
 
+#if VTE_GTK == 3
+
 static void
 vte_terminal_style_updated (GtkWidget *widget) noexcept
 try
@@ -298,7 +315,7 @@ try
                }
        }
 
-        return WIDGET(terminal)->key_press(event);
+        return WIDGET(terminal)->event_key_press(event);
 }
 catch (...)
 {
@@ -312,7 +329,7 @@ vte_terminal_key_release(GtkWidget *widget,
 try
 {
        VteTerminal *terminal = VTE_TERMINAL(widget);
-        return WIDGET(terminal)->key_release(event);
+        return WIDGET(terminal)->event_key_release(event);
 }
 catch (...)
 {
@@ -326,7 +343,7 @@ vte_terminal_motion_notify(GtkWidget *widget,
 try
 {
         VteTerminal *terminal = VTE_TERMINAL(widget);
-        return WIDGET(terminal)->motion_notify(event);
+        return WIDGET(terminal)->event_motion_notify(event);
 }
 catch (...)
 {
@@ -340,7 +357,7 @@ vte_terminal_button_press(GtkWidget *widget,
 try
 {
        VteTerminal *terminal = VTE_TERMINAL(widget);
-        return WIDGET(terminal)->button_press(event);
+        return WIDGET(terminal)->event_button_press(event);
 }
 catch (...)
 {
@@ -354,7 +371,7 @@ vte_terminal_button_release(GtkWidget *widget,
 try
 {
        VteTerminal *terminal = VTE_TERMINAL(widget);
-        return WIDGET(terminal)->button_release(event);
+        return WIDGET(terminal)->event_button_release(event);
 }
 catch (...)
 {
@@ -368,7 +385,7 @@ vte_terminal_scroll(GtkWidget *widget,
 try
 {
         auto terminal = VTE_TERMINAL(widget);
-        return WIDGET(terminal)->scroll(event);
+        return WIDGET(terminal)->event_scroll(event);
 }
 catch (...)
 {
@@ -382,7 +399,7 @@ vte_terminal_focus_in(GtkWidget *widget,
 try
 {
        VteTerminal *terminal = VTE_TERMINAL(widget);
-        WIDGET(terminal)->focus_in(event);
+        WIDGET(terminal)->event_focus_in(event);
         return FALSE;
 }
 catch (...)
@@ -397,7 +414,7 @@ vte_terminal_focus_out(GtkWidget *widget,
 try
 {
        VteTerminal *terminal = VTE_TERMINAL(widget);
-        WIDGET(terminal)->focus_out(event);
+        WIDGET(terminal)->event_focus_out(event);
         return FALSE;
 }
 catch (...)
@@ -418,7 +435,7 @@ try
                ret = GTK_WIDGET_CLASS (vte_terminal_parent_class)->enter_notify_event (widget, event);
        }
 
-        WIDGET(terminal)->enter(event);
+        WIDGET(terminal)->event_enter(event);
 
         return ret;
 }
@@ -440,7 +457,7 @@ try
                ret = GTK_WIDGET_CLASS (vte_terminal_parent_class)->leave_notify_event (widget, event);
        }
 
-        WIDGET(terminal)->leave(event);
+        WIDGET(terminal)->event_leave(event);
 
         return ret;
 }
@@ -478,33 +495,7 @@ catch (...)
         vte::log_exception();
 }
 
-static void
-vte_terminal_size_allocate(GtkWidget *widget,
-                           GtkAllocation *allocation) noexcept
-try
-{
-       VteTerminal *terminal = VTE_TERMINAL(widget);
-        WIDGET(terminal)->size_allocate(allocation);
-}
-catch (...)
-{
-        vte::log_exception();
-}
-
-static gboolean
-vte_terminal_draw(GtkWidget *widget,
-                  cairo_t *cr) noexcept
-try
-{
-        VteTerminal *terminal = VTE_TERMINAL (widget);
-        WIDGET(terminal)->draw(cr);
-        return FALSE;
-}
-catch (...)
-{
-        vte::log_exception();
-        return false;
-}
+#endif /* VTE_GTK == 3 */
 
 static void
 vte_terminal_realize(GtkWidget *widget) noexcept
@@ -569,16 +560,105 @@ vte_terminal_unmap(GtkWidget *widget) noexcept
 }
 
 static void
-vte_terminal_screen_changed (GtkWidget *widget,
-                             GdkScreen *previous_screen) noexcept
+vte_terminal_state_flags_changed(GtkWidget* widget,
+                                 GtkStateFlags old_flags) noexcept
 try
 {
-        VteTerminal *terminal = VTE_TERMINAL (widget);
+        GTK_WIDGET_CLASS(vte_terminal_parent_class)->state_flags_changed(widget, old_flags);
 
-        if (GTK_WIDGET_CLASS (vte_terminal_parent_class)->screen_changed) {
-                GTK_WIDGET_CLASS (vte_terminal_parent_class)->screen_changed (widget, previous_screen);
-        }
+        auto terminal = VTE_TERMINAL(widget);
+        WIDGET(terminal)->state_flags_changed(old_flags);
+}
+catch (...)
+{
+        vte::log_exception();
+}
+
+static void
+vte_terminal_direction_changed(GtkWidget* widget,
+                               GtkTextDirection old_direction) noexcept
+try
+{
+        auto const parent_class = GTK_WIDGET_CLASS(vte_terminal_parent_class);
+        if (parent_class->direction_changed)
+                parent_class->direction_changed(widget, old_direction);
+
+        auto terminal = VTE_TERMINAL(widget);
+        WIDGET(terminal)->direction_changed(old_direction);
+}
+catch (...)
+{
+        vte::log_exception();
+}
 
+static GtkSizeRequestMode
+vte_terminal_get_request_mode(GtkWidget* widget) noexcept
+{
+        return GTK_SIZE_REQUEST_CONSTANT_SIZE;
+}
+
+static gboolean
+vte_terminal_query_tooltip(GtkWidget* widget,
+                           int x,
+                           int y,
+                           gboolean keyboard,
+                           GtkTooltip* tooltip) noexcept
+try
+{
+        auto const parent_class = GTK_WIDGET_CLASS(vte_terminal_parent_class);
+        if (parent_class->query_tooltip(widget, x, y, keyboard, tooltip))
+                return true;
+
+        auto terminal = VTE_TERMINAL(widget);
+        return WIDGET(terminal)->query_tooltip(x, y, keyboard, tooltip);
+}
+catch (...)
+{
+        vte::log_exception();
+        return false;
+}
+
+
+#if VTE_GTK == 3
+
+static void
+vte_terminal_size_allocate(GtkWidget* widget,
+                           GtkAllocation* allocation) noexcept
+try
+{
+        auto terminal = VTE_TERMINAL(widget);
+        WIDGET(terminal)->size_allocate(allocation);
+}
+catch (...)
+{
+        vte::log_exception();
+}
+
+static gboolean
+vte_terminal_draw(GtkWidget* widget,
+                  cairo_t* cr) noexcept
+try
+{
+        auto terminal = VTE_TERMINAL(widget);
+        WIDGET(terminal)->draw(cr);
+        return FALSE;
+}
+catch (...)
+{
+        vte::log_exception();
+        return false;
+}
+
+static void
+vte_terminal_screen_changed(GtkWidget* widget,
+                            GdkScreen* previous_screen) noexcept
+try
+{
+        auto const parent_class = GTK_WIDGET_CLASS(vte_terminal_parent_class);
+        if (parent_class->screen_changed)
+                parent_class->screen_changed(widget, previous_screen);
+
+       auto terminal = VTE_TERMINAL(widget);
         WIDGET(terminal)->screen_changed(previous_screen);
 }
 catch (...)
@@ -586,6 +666,156 @@ catch (...)
         vte::log_exception();
 }
 
+#endif /* VTE_GTK == 3 */
+
+#if VTE_GTK == 4
+
+static void
+vte_terminal_size_allocate(GtkWidget *widget,
+                           int width,
+                           int height,
+                           int baseline) noexcept
+try
+{
+        GTK_WIDGET_CLASS(vte_terminal_parent_class)->size_allocate(widget, width, height, baseline);
+
+       auto terminal = VTE_TERMINAL(widget);
+        WIDGET(terminal)->size_allocate(width, height, baseline);
+}
+catch (...)
+{
+        vte::log_exception();
+}
+
+static void
+vte_terminal_root(GtkWidget *widget) noexcept
+try
+{
+        GTK_WIDGET_CLASS(vte_terminal_parent_class)->root(widget);
+
+        auto terminal = VTE_TERMINAL(widget);
+        WIDGET(terminal)->root();
+}
+catch (...)
+{
+        vte::log_exception();
+}
+
+static void
+vte_terminal_unroot(GtkWidget *widget) noexcept
+{
+        _vte_debug_print(VTE_DEBUG_LIFECYCLE, "vte_terminal_unroot()\n");
+
+        auto terminal = VTE_TERMINAL(widget);
+        WIDGET(terminal)->unroot();
+
+        GTK_WIDGET_CLASS(vte_terminal_parent_class)->unroot(widget);
+}
+
+static void
+vte_terminal_measure(GtkWidget* widget,
+                     GtkOrientation orientation,
+                     int for_size,
+                     int* minimum,
+                     int* natural,
+                     int* minimum_baseline,
+                     int* natural_baseline) noexcept
+try
+{
+        auto terminal = VTE_TERMINAL(widget);
+        WIDGET(terminal)->measure(orientation, for_size,
+                                  minimum, natural,
+                                  minimum_baseline, natural_baseline);
+}
+catch (...)
+{
+        vte::log_exception();
+}
+
+static void
+vte_terminal_compute_expand(GtkWidget* widget,
+                            gboolean* hexpand,
+                            gboolean* vexpand) noexcept
+try
+{
+        auto terminal = VTE_TERMINAL(widget);
+        auto [h, v] = WIDGET(terminal)->compute_expand();
+        *hexpand = h;
+        *vexpand = v;
+}
+catch (...)
+{
+        vte::log_exception();
+        *hexpand = *vexpand = false;
+}
+
+static void
+vte_terminal_css_changed(GtkWidget* widget,
+                         GtkCssStyleChange* change) noexcept
+try
+{
+        GTK_WIDGET_CLASS(vte_terminal_parent_class)->css_changed(widget, change);
+        auto terminal = VTE_TERMINAL(widget);
+        WIDGET(terminal)->css_changed(change);
+}
+catch (...)
+{
+        vte::log_exception();
+}
+
+static void
+vte_terminal_system_setting_changed(GtkWidget* widget,
+                                    GtkSystemSetting setting) noexcept
+try
+{
+        GTK_WIDGET_CLASS(vte_terminal_parent_class)->system_setting_changed(widget, setting);
+        auto terminal = VTE_TERMINAL(widget);
+        WIDGET(terminal)->system_setting_changed(setting);
+}
+catch (...)
+{
+        vte::log_exception();
+}
+
+static void
+vte_terminal_snapshot(GtkWidget* widget,
+                      GtkSnapshot* snapshot_object) noexcept
+try
+{
+        GTK_WIDGET_CLASS(vte_terminal_parent_class)->snapshot(widget, snapshot_object);
+        auto terminal = VTE_TERMINAL(widget);
+        WIDGET(terminal)->snapshot(snapshot_object);
+}
+catch (...)
+{
+        vte::log_exception();
+}
+
+static gboolean
+vte_terminal_contains(GtkWidget* widget,
+                      double x,
+                      double y) noexcept
+try
+{
+        auto terminal = VTE_TERMINAL(widget);
+        if (WIDGET(terminal)->contains(x, y))
+                return true;
+
+        auto const parent_class = GTK_WIDGET_CLASS(vte_terminal_parent_class);
+        if (parent_class->contains &&
+            parent_class->contains(widget, x, y))
+                return true;
+
+        return false;
+}
+catch (...)
+{
+        vte::log_exception();
+        return false;
+}
+
+#endif /* VTE_GTK == 4 */
+
 static void
 vte_terminal_constructed (GObject *object) noexcept
 try
@@ -615,7 +845,9 @@ try
                                         VTE_TERMINAL_GET_CLASS (terminal)->priv->style_provider,
                                         GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
 
+#if VTE_GTK == 3
         gtk_widget_set_has_window(&terminal->widget, FALSE);
+#endif
 
        place = vte_terminal_get_instance_private(terminal);
         new (place) VteTerminalPrivate{terminal};
@@ -916,10 +1148,6 @@ catch (...)
 static void
 vte_terminal_class_init(VteTerminalClass *klass)
 {
-       GObjectClass *gobject_class;
-       GtkWidgetClass *widget_class;
-       GtkBindingSet  *binding_set;
-
 #ifdef VTE_DEBUG
        {
                 _vte_debug_init();
@@ -943,13 +1171,15 @@ vte_terminal_class_init(VteTerminalClass *klass)
        }
 #endif
 
+#if VTE_GTK == 3
        _VTE_DEBUG_IF (VTE_DEBUG_UPDATES) gdk_window_set_debug_updates(TRUE);
+#endif
 
        bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);
        bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
 
-       gobject_class = G_OBJECT_CLASS(klass);
-       widget_class = GTK_WIDGET_CLASS(klass);
+       auto gobject_class = G_OBJECT_CLASS(klass);
+       auto widget_class = GTK_WIDGET_CLASS(klass);
 
        /* Override some of the default handlers. */
         gobject_class->constructed = vte_terminal_constructed;
@@ -957,12 +1187,21 @@ vte_terminal_class_init(VteTerminalClass *klass)
        gobject_class->finalize = vte_terminal_finalize;
         gobject_class->get_property = vte_terminal_get_property;
         gobject_class->set_property = vte_terminal_set_property;
+
        widget_class->realize = vte_terminal_realize;
        widget_class->unrealize = vte_terminal_unrealize;
         widget_class->map = vte_terminal_map;
         widget_class->unmap = vte_terminal_unmap;
-       widget_class->scroll_event = vte_terminal_scroll;
+
+       widget_class->size_allocate = vte_terminal_size_allocate;
+        widget_class->state_flags_changed = vte_terminal_state_flags_changed;
+        widget_class->direction_changed = vte_terminal_direction_changed;
+        widget_class->get_request_mode = vte_terminal_get_request_mode;
+        widget_class->query_tooltip = vte_terminal_query_tooltip;
+
+#if VTE_GTK == 3
         widget_class->draw = vte_terminal_draw;
+       widget_class->scroll_event = vte_terminal_scroll;
        widget_class->key_press_event = vte_terminal_key_press;
        widget_class->key_release_event = vte_terminal_key_release;
        widget_class->button_press_event = vte_terminal_button_press;
@@ -975,8 +1214,19 @@ vte_terminal_class_init(VteTerminalClass *klass)
        widget_class->style_updated = vte_terminal_style_updated;
        widget_class->get_preferred_width = vte_terminal_get_preferred_width;
        widget_class->get_preferred_height = vte_terminal_get_preferred_height;
-       widget_class->size_allocate = vte_terminal_size_allocate;
         widget_class->screen_changed = vte_terminal_screen_changed;
+#endif
+
+#if VTE_GTK == 4
+        widget_class->root = vte_terminal_root;
+        widget_class->unroot = vte_terminal_unroot;
+        widget_class->measure = vte_terminal_measure;
+        widget_class->compute_expand = vte_terminal_compute_expand;
+        widget_class->css_changed = vte_terminal_css_changed;
+        widget_class->system_setting_changed = vte_terminal_system_setting_changed;
+        widget_class->snapshot = vte_terminal_snapshot;
+        widget_class->contains = vte_terminal_contains;
+#endif
 
         gtk_widget_class_set_css_name(widget_class, VTE_TERMINAL_CSS_NAME);
 
@@ -2077,23 +2327,33 @@ vte_terminal_class_init(VteTerminalClass *klass)
 
         g_object_class_install_properties(gobject_class, LAST_PROP, pspecs);
 
+#if VTE_GTK == 3
        /* Disable GtkWidget's keybindings except for Shift-F10 and MenuKey
          * which pop up the context menu.
          */
-       binding_set = gtk_binding_set_by_class(vte_terminal_parent_class);
+       auto const binding_set = gtk_binding_set_by_class(vte_terminal_parent_class);
        gtk_binding_entry_skip(binding_set, GDK_KEY_F1, GDK_CONTROL_MASK);
        gtk_binding_entry_skip(binding_set, GDK_KEY_F1, GDK_SHIFT_MASK);
        gtk_binding_entry_skip(binding_set, GDK_KEY_KP_F1, GDK_CONTROL_MASK);
        gtk_binding_entry_skip(binding_set, GDK_KEY_KP_F1, GDK_SHIFT_MASK);
+#endif /* VTE_GTK == 3 */
 
         process_timer = g_timer_new();
 
         klass->priv = G_TYPE_CLASS_GET_PRIVATE (klass, VTE_TYPE_TERMINAL, VteTerminalClassPrivate);
 
         klass->priv->style_provider = GTK_STYLE_PROVIDER (gtk_css_provider_new ());
+#if VTE_GTK == 3
+        auto err = vte::glib::Error{};
+#elif VTE_GTK == 4
+        g_signal_connect(klass->priv->style_provider, "parsing-error",
+                         G_CALLBACK(style_provider_parsing_error_cb), nullptr);
+#endif
         gtk_css_provider_load_from_data (GTK_CSS_PROVIDER (klass->priv->style_provider),
                                          "VteTerminal, " VTE_TERMINAL_CSS_NAME " {\n"
+#if VTE_GTK == 3
                                          "padding: 1px 1px 1px 1px;\n"
+#endif
 #if GTK_CHECK_VERSION (3, 24, 22)
                                          "background-color: @text_view_bg;\n"
 #else
@@ -2101,12 +2361,21 @@ vte_terminal_class_init(VteTerminalClass *klass)
 #endif
                                          "color: @theme_text_color;\n"
                                          "}\n",
-                                         -1, NULL);
+                                         -1
+#if VTE_GTK == 3
+                                         , NULL
+#endif
+                                         );
+#if VTE_GTK == 3
+        err.assert_no_error();
+#endif
 
+#if VTE_GTK == 3
 #ifdef WITH_A11Y
         /* a11y */
         gtk_widget_class_set_accessible_type(widget_class, VTE_TYPE_TERMINAL_ACCESSIBLE);
 #endif
+#endif
 }
 
 static gboolean
@@ -2504,6 +2773,8 @@ catch (...)
         vte::log_exception();
 }
 
+#if VTE_GTK == 3
+
 /**
  * vte_terminal_match_add_gregex:
  * @terminal: a #VteTerminal
@@ -2524,6 +2795,8 @@ vte_terminal_match_add_gregex(VteTerminal *terminal,
         return -1;
 }
 
+#endif /* VTE_GTK == 3 */
+
 /**
  * vte_terminal_match_add_regex:
  * @terminal: a #VteTerminal
@@ -2600,6 +2873,8 @@ catch (...)
         return nullptr;
 }
 
+#if VTE_GTK == 3
+
 /**
  * vte_terminal_match_check_event:
  * @terminal: a #VteTerminal
@@ -2795,6 +3070,8 @@ vte_terminal_event_check_gregex_simple(VteTerminal *terminal,
         return FALSE;
 }
 
+#endif /* VTE_GTK == 3 */
+
 /**
  * vte_terminal_match_set_cursor:
  * @terminal: a #VteTerminal
@@ -2823,6 +3100,8 @@ catch (...)
         vte::log_exception();
 }
 
+#if VTE_GTK == 3
+
 /**
  * vte_terminal_match_set_cursor_type:
  * @terminal: a #VteTerminal
@@ -2849,6 +3128,7 @@ catch (...)
 {
         vte::log_exception();
 }
+#endif /* VTE_GTK == 3 */
 
 /**
  * vte_terminal_match_set_cursor_name:
@@ -3011,6 +3291,8 @@ catch (...)
         return nullptr;
 }
 
+#if VTE_GTK == 3
+
 /**
  * vte_terminal_search_set_gregex:
  * @terminal: a #VteTerminal
@@ -3044,6 +3326,8 @@ vte_terminal_search_get_gregex (VteTerminal *terminal) noexcept
         return nullptr;
 }
 
+#endif /* VTE_GTK == 3 */
+
 /**
  * vte_terminal_search_set_wrap_around:
  * @terminal: a #VteTerminal
@@ -5046,6 +5330,8 @@ catch (...)
         vte::log_exception();
 }
 
+#if VTE_GTK == 3
+
 /* Just some arbitrary minimum values */
 #define MIN_COLUMNS (16)
 #define MIN_ROWS    (2)
@@ -5136,6 +5422,8 @@ vte_terminal_set_geometry_hints_for_window(VteTerminal *terminal,
                                                        GDK_HINT_BASE_SIZE));
 }
 
+#endif /* VTE_GTK == 3 */
+
 /**
  * vte_terminal_get_has_selection:
  * @terminal: a #VteTerminal
@@ -5698,6 +5986,8 @@ catch (...)
         return vte::glib::set_error_from_exception(error);
 }
 
+#if VTE_GTK == 3
+
 /**
  * vte_terminal_set_clear_background:
  * @terminal: a #VteTerminal
@@ -5764,6 +6054,8 @@ catch (...)
         *color = {0., 0., 0., 1.};
 }
 
+#endif /* VTE_GTK == 3 */
+
 /**
  * vte_terminal_set_enable_sixel:
  * @terminal: a #VteTerminal
diff --git a/src/vteinternal.hh b/src/vteinternal.hh
index 41766c0e..a6ad315b 100644
--- a/src/vteinternal.hh
+++ b/src/vteinternal.hh
@@ -89,9 +89,14 @@ namespace platform {
  * Holds a platform cursor. This is either a named cursor (string),
  * a reference to a GdkCursor*, or a cursor type.
  */
+#if VTE_GTK == 3
 using Cursor = std::variant<std::string,
                             vte::glib::RefPtr<GdkCursor>,
                             GdkCursorType>;
+#elif VTE_GTK == 4
+using Cursor = std::variant<std::string,
+                            vte::glib::RefPtr<GdkCursor>>;
+#endif
 
 } // namespace platform
 } // namespace vte
@@ -427,9 +432,10 @@ public:
                                               "cursor-blink-timer"};
         CursorBlinkMode m_cursor_blink_mode{CursorBlinkMode::eSYSTEM};
         bool m_cursor_blink_state{false};
-        bool m_cursor_blinks{false};           /* whether the cursor is actually blinking */
-        gint m_cursor_blink_cycle;          /* gtk-cursor-blink-time / 2 */
-        int m_cursor_blink_timeout{500};        /* gtk-cursor-blink-timeout */
+        bool m_cursor_blinks{false};        /* whether the cursor is actually blinking */
+        bool m_cursor_blinks_system{true};  /* gtk-cursor-blink */
+        gint m_cursor_blink_cycle{1000};    /* gtk-cursor-blink-time / 2 */
+        int m_cursor_blink_timeout{500};    /* gtk-cursor-blink-timeout */
         gint64 m_cursor_blink_time;         /* how long the cursor has been blinking yet */
         bool m_has_focus{false};            /* is the widget focused */
 
@@ -691,7 +697,11 @@ public:
         double m_undercurl_thickness{VTE_LINE_WIDTH};
 
         /* Style stuff */
+#if VTE_GTK == 3
         GtkBorder m_padding{1, 1, 1, 1};
+#elif VTE_GTK == 4
+        GtkBorder m_padding{0, 0, 0, 0};
+#endif
         auto padding() const noexcept { return &m_padding; }
 
         vte::glib::RefPtr<GtkAdjustment> m_vadjustment{};
@@ -806,6 +816,9 @@ public:
 
         /* The allocation of the widget */
         cairo_rectangle_int_t m_allocated_rect;
+
+        constexpr auto const* allocated_rect() const noexcept { return &m_allocated_rect; }
+
         /* The usable view area. This is the allocation, minus the padding, but
          * including additional right/bottom area if the allocation is not grid aligned.
          */
@@ -872,17 +885,32 @@ public:
         void widget_mouse_enter(vte::platform::MouseEvent const& event);
         void widget_mouse_leave(vte::platform::MouseEvent const& event);
         bool widget_mouse_scroll(vte::platform::ScrollEvent const& event);
+#if VTE_GTK == 3
         void widget_draw(cairo_t *cr);
-        void widget_get_preferred_width(int *minimum_width,
-                                        int *natural_width);
-        void widget_get_preferred_height(int *minimum_height,
-                                         int *natural_height);
-        void widget_size_allocate(GtkAllocation *allocation);
+#endif /* VTE_GTK == 3 */
+        void widget_measure_width(int *minimum_width,
+                                  int *natural_width);
+        void widget_measure_height(int *minimum_height,
+                                   int *natural_height);
+
+#if VTE_GTK == 3
+        void widget_size_allocate(int x,
+                                  int y,
+                                  int width,
+                                  int height,
+                                  int baseline);
+#elif VTE_GTK == 4
+        void widget_size_allocate(int width,
+                                  int height,
+                                  int baseline);
+#endif /* VTE_GTK */
 
         void set_blink_settings(bool blink,
                                 int blink_time,
                                 int blink_timeout) noexcept;
 
+        void draw(cairo_t *cr,
+                  cairo_region_t const* region);
         void paint_cursor();
         void paint_im_preedit_string();
         void draw_cells(vte::view::DrawingContext::TextRequest* items,
@@ -1119,26 +1147,53 @@ public:
         bool rowcol_from_event(vte::platform::MouseEvent const& event,
                                long *column,
                                long *row);
+#if VTE_GTK == 4
+        bool rowcol_at(double x,
+                       double y,
+                       long* column,
+                       long* row);
+#endif
 
         char *hyperlink_check(vte::platform::MouseEvent const& event);
+        char *hyperlink_check_at(double x,
+                                 double y);
+        char *hyperlink_check(vte::grid::column_t column,
+                              vte::grid::row_t row);
 
         bool regex_match_check_extra(vte::platform::MouseEvent const& event,
                                      vte::base::Regex const** regexes,
                                      size_t n_regexes,
                                      uint32_t match_flags,
                                      char** matches);
+        bool regex_match_check_extra_at(double x,
+                                        double y,
+                                        vte::base::Regex const** regexes,
+                                        size_t n_regexes,
+                                        uint32_t match_flags,
+                                        char** matches);
+        bool regex_match_check_extra(vte::grid::column_t column,
+                                     vte::grid::row_t row,
+                                     vte::base::Regex const** regexes,
+                                     size_t n_regexes,
+                                     uint32_t match_flags,
+                                     char** matches);
 
         char *regex_match_check(vte::grid::column_t column,
                                 vte::grid::row_t row,
                                 int *tag);
         char *regex_match_check(vte::platform::MouseEvent const& event,
                                 int *tag);
+        char *regex_match_check_at(double x,
+                                   double y,
+                                   int *tag);
         void regex_match_remove(int tag) noexcept;
         void regex_match_remove_all() noexcept;
         void regex_match_set_cursor(int tag,
                                     GdkCursor *gdk_cursor);
+        #if VTE_GTK == 3
         void regex_match_set_cursor(int tag,
                                     GdkCursorType cursor_type);
+        #endif
         void regex_match_set_cursor(int tag,
                                     char const* cursor_name);
         bool match_rowcol_to_offset(vte::grid::column_t column,
diff --git a/src/vteseq.cc b/src/vteseq.cc
index 1b76ff22..2a8d777d 100644
--- a/src/vteseq.cc
+++ b/src/vteseq.cc
@@ -9140,12 +9140,17 @@ Terminal::XTERM_WM(vte::parser::Sequence const& seq)
                 /* FIMXE: this should really report the monitor's workarea,
                  * or even just a fixed value.
                  */
+#if VTE_GTK == 3
                 auto gdkscreen = gtk_widget_get_screen(m_widget);
                 int height = gdk_screen_get_height(gdkscreen);
                 int width = gdk_screen_get_width(gdkscreen);
                 _vte_debug_print(VTE_DEBUG_EMULATION,
                                  "Reporting screen size as %dx%d cells.\n",
                                  height / int(m_cell_height), width / int(m_cell_width));
+#elif VTE_GTK == 4
+                auto height = int(m_row_count * m_cell_height);
+                auto width = int(m_column_count * m_cell_width);
+#endif
 
                 reply(seq, VTE_REPLY_XTERM_WM,
                       {9, height / int(m_cell_height), width / int(m_cell_width)});
diff --git a/src/widget.cc b/src/widget.cc
index 25e4b242..d6ebfa86 100644
--- a/src/widget.cc
+++ b/src/widget.cc
@@ -32,6 +32,16 @@
 #include "vteptyinternal.hh"
 #include "debug.h"
 
+#if VTE_GTK == 4
+#include "graphene-glue.hh"
+#endif
+
+#if VTE_GTK == 3
+#define VTE_STYLE_CLASS_MONOSPACE GTK_STYLE_CLASS_MONOSPACE
+#elif VTE_GTK == 4
+#define VTE_STYLE_CLASS_MONOSPACE "monospace"
+#endif
+
 using namespace std::literals;
 
 namespace vte {
@@ -142,11 +152,19 @@ Widget::Widget(VteTerminal* t)
           m_hscroll_policy{GTK_SCROLL_NATURAL},
           m_vscroll_policy{GTK_SCROLL_NATURAL}
 {
+#if VTE_GTK == 3
         gtk_widget_set_can_focus(gtk(), true);
+#endif
+
+#if VTE_GTK == 4
+        gtk_widget_set_focusable(gtk(), true);
+#endif
 
+#if VTE_GTK == 3
         /* We do our own redrawing. */
         // FIXMEchpe is this still necessary?
         gtk_widget_set_redraw_on_allocate(gtk(), false);
+#endif
 
         /* Until Terminal init is completely fixed, use zero'd memory */
         auto place = g_malloc0(sizeof(vte::terminal::Terminal));
@@ -156,7 +174,7 @@ Widget::Widget(VteTerminal* t)
 Widget::~Widget() noexcept
 try
 {
-        g_signal_handlers_disconnect_matched(gtk_widget_get_settings(m_widget),
+        g_signal_handlers_disconnect_matched(m_settings.get(),
                                              G_SIGNAL_MATCH_DATA,
                                              0, 0, NULL, NULL,
                                              this);
@@ -175,19 +193,38 @@ void
 Widget::beep() noexcept
 {
         if (realized())
-                gdk_window_beep(gtk_widget_get_window(m_widget));
+                gtk_widget_error_bell(gtk());
 }
 
+#if VTE_GTK == 4
+
+bool
+Widget::contains(double x,
+                 double y)
+{
+        return false;
+}
+
+#endif /* VTE_GTK == 4 */
+
 vte::glib::RefPtr<GdkCursor>
 Widget::create_cursor(std::string const& name) const noexcept
 {
+#if VTE_GTK == 3
        return vte::glib::take_ref(gdk_cursor_new_from_name(gtk_widget_get_display(m_widget), name.c_str()));
+#elif VTE_GTK == 4
+        return vte::glib::take_ref(gdk_cursor_new_from_name(name.c_str(), nullptr /* fallback */));
+#endif
 }
 
 void
 Widget::set_cursor(GdkCursor* cursor) noexcept
 {
+#if VTE_GTK == 3
         gdk_window_set_cursor(m_event_window, cursor);
+#elif VTE_GTK == 4
+        gtk_widget_set_cursor(gtk(), cursor);
+#endif
 }
 
 void
@@ -196,24 +233,36 @@ Widget::set_cursor(Cursor const& cursor) noexcept
         if (!realized())
                 return;
 
-        auto display = gtk_widget_get_display(m_widget);
         GdkCursor* gdk_cursor{nullptr};
         switch (cursor.index()) {
         case 0:
-                gdk_cursor = gdk_cursor_new_from_name(display, std::get<0>(cursor).c_str());
+#if VTE_GTK == 3
+                gdk_cursor = gdk_cursor_new_from_name(gtk_widget_get_display(gtk()),
+                                                      std::get<0>(cursor).c_str());
+#elif VTE_GTK == 4
+                gdk_cursor = gdk_cursor_new_from_name(std::get<0>(cursor).c_str(),
+                                                      nullptr /* fallback */);
+#endif /* VTE_GTK */
                 break;
+
         case 1:
                 gdk_cursor = std::get<1>(cursor).get();
-                if (gdk_cursor != nullptr &&
-                    gdk_cursor_get_display(gdk_cursor) == display) {
+                if (gdk_cursor != nullptr
+#if VTE_GTK == 3
+                    && gdk_cursor_get_display(gdk_cursor) == gtk_widget_get_display(gtk())
+#endif
+                ) {
                         g_object_ref(gdk_cursor);
                 } else {
                         gdk_cursor = nullptr;
                 }
                 break;
+
+#if VTE_GTK == 3
         case 2:
-                gdk_cursor = gdk_cursor_new_for_display(display, std::get<2>(cursor));
+                gdk_cursor = gdk_cursor_new_for_display(gtk_widget_get_display(gtk()), std::get<2>(cursor));
                 break;
+#endif
         }
 
         set_cursor(gdk_cursor);
@@ -290,15 +339,60 @@ Widget::clipboard_set_text(ClipboardType type,
         clipboard_get(type).set_text(str);
 }
 
+#if VTE_GTK == 4
+
+std::pair<bool, bool>
+Widget::compute_expand()
+{
+        return {true, true};
+}
+
+#endif /* VTE_GTK == 4 */
+
 void
 Widget::constructed() noexcept
 {
+#if VTE_GTK == 3
+        auto context = gtk_widget_get_style_context(m_widget);
+        gtk_style_context_add_class (context, VTE_STYLE_CLASS_MONOSPACE);
+#elif VTE_GTK == 4
+        gtk_widget_add_css_class(gtk(), VTE_STYLE_CLASS_MONOSPACE);
+#endif /* VTE_GTK */
+
+#if VTE_GTK == 4
+
+        connect_settings();
+
+#endif /* VTE_GTK == 4 */
+
+#if VTE_GTK == 3
         /* Set the style as early as possible, before GTK+ starts
          * invoking various callbacks. This is needed in order to
          * compute the initial geometry correctly in presence of
          * non-default padding, see bug 787710.
          */
         style_updated();
+#elif VTE_GTK == 4
+        padding_changed();
+#endif /* VTE_GTK  */
+}
+
+#if VTE_GTK == 4
+
+void
+Widget::css_changed(GtkCssStyleChange* change)
+{
+        /* This function is mostly useless, since there's no public API for GtkCssStyleChange */
+
+        padding_changed();
+}
+
+#endif /* VTE_GTK == 4 */
+
+void
+Widget::direction_changed(GtkTextDirection old_direction) noexcept
+{
+        // FIXME: does this need to feed to BiDi somehow?
 }
 
 void
@@ -330,9 +424,122 @@ Widget::im_filter_keypress(KeyEvent const& event) noexcept
         // FIXMEchpe this can only be called when realized, so the m_im_context check is redundant
         return m_im_context &&
                 gtk_im_context_filter_keypress(m_im_context.get(),
-                                               reinterpret_cast<GdkEventKey*>(event.platform_event()));
+#if VTE_GTK == 3
+                                               reinterpret_cast<GdkEventKey*>(event.platform_event())
+#elif VTE_GTK == 4
+                                               event.platform_event()
+#endif
+                                               );
 }
 
+#if VTE_GTK == 3
+
+void
+Widget::event_focus_in(GdkEventFocus *event)
+{
+       _vte_debug_print(VTE_DEBUG_EVENTS, "Focus In");
+
+        m_terminal->widget_focus_in();
+}
+
+void
+Widget::event_focus_out(GdkEventFocus *event)
+{
+       _vte_debug_print(VTE_DEBUG_EVENTS, "Focus Out");
+
+        m_terminal->widget_focus_out();
+}
+
+bool
+Widget::event_key_press(GdkEventKey *event)
+{
+        auto key_event = key_event_from_gdk(reinterpret_cast<GdkEvent*>(event));
+
+       _vte_debug_print(VTE_DEBUG_EVENTS, "Key press key=%x keycode=%x modifiers=%x\n",
+                         key_event.keyval(), key_event.keycode(), key_event.modifiers());
+
+        return m_terminal->widget_key_press(key_event);
+}
+
+bool
+Widget::event_key_release(GdkEventKey *event)
+{
+        auto key_event = key_event_from_gdk(reinterpret_cast<GdkEvent*>(event));
+
+       _vte_debug_print(VTE_DEBUG_EVENTS, "Key release key=%x keycode=%x modifiers=%x\n",
+                         key_event.keyval(), key_event.keycode(), key_event.modifiers());
+
+        return m_terminal->widget_key_release(key_event);
+}
+
+bool
+Widget::event_button_press(GdkEventButton *event)
+{
+        auto mouse_event = mouse_event_from_gdk(reinterpret_cast<GdkEvent*>(event));
+
+       _vte_debug_print(VTE_DEBUG_EVENTS, "Click press button=%d press_count=%d x=%.3f y=%.3f\n",
+                         mouse_event.button_value(), mouse_event.press_count(),
+                         mouse_event.x(), mouse_event.y());
+
+        return m_terminal->widget_mouse_press(mouse_event);
+}
+
+bool
+Widget::event_button_release(GdkEventButton *event)
+{
+        auto mouse_event = mouse_event_from_gdk(reinterpret_cast<GdkEvent*>(event));
+
+       _vte_debug_print(VTE_DEBUG_EVENTS, "Click release button=%d x=%.3f y=%.3f\n",
+                         mouse_event.button_value(), mouse_event.x(), mouse_event.y());
+
+        return m_terminal->widget_mouse_release(mouse_event);
+}
+
+void
+Widget::event_enter(GdkEventCrossing *event)
+{
+        auto mouse_event = mouse_event_from_gdk(reinterpret_cast<GdkEvent*>(event));
+
+       _vte_debug_print(VTE_DEBUG_EVENTS, "Motion enter x=%.3f y=%.3f\n",
+                         mouse_event.x(), mouse_event.y());
+
+        m_terminal->widget_mouse_enter(mouse_event);
+}
+
+void
+Widget::event_leave(GdkEventCrossing *event)
+{
+        auto mouse_event = mouse_event_from_gdk(reinterpret_cast<GdkEvent*>(event));
+
+       _vte_debug_print(VTE_DEBUG_EVENTS, "Motion leave x=%.3f y=%.3f\n",
+                         mouse_event.x(), mouse_event.y());
+
+        m_terminal->widget_mouse_leave(mouse_event);
+}
+bool
+Widget::event_scroll(GdkEventScroll *event)
+{
+        auto scroll_event = scroll_event_from_gdk(reinterpret_cast<GdkEvent*>(event));
+
+       _vte_debug_print(VTE_DEBUG_EVENTS, "Scroll delta_x=%.3f delta_y=%.3f\n",
+                         scroll_event.dx(), scroll_event.dy());
+
+        return m_terminal->widget_mouse_scroll(scroll_event);
+}
+
+bool
+Widget::event_motion_notify(GdkEventMotion *event)
+{
+        auto mouse_event = mouse_event_from_gdk(reinterpret_cast<GdkEvent*>(event));
+
+       _vte_debug_print(VTE_DEBUG_EVENTS, "Motion x=%.3f y=%.3f\n",
+                         mouse_event.x(), mouse_event.y());
+
+        return m_terminal->widget_mouse_motion(mouse_event);
+}
+
+#endif /* VTE_GTK == 3 */
+
 void
 Widget::im_focus_in() noexcept
 {
@@ -368,6 +575,8 @@ Widget::im_set_cursor_location(cairo_rectangle_int_t const* rect) noexcept
         gtk_im_context_set_cursor_location(m_im_context.get(), rect);
 }
 
+#if VTE_GTK == 3
+
 unsigned
 Widget::read_modifiers_from_gdk(GdkEvent* event) const noexcept
 {
@@ -376,11 +585,9 @@ Widget::read_modifiers_from_gdk(GdkEvent* event) const noexcept
         if (!gdk_event_get_state(event, &mods))
                 return 0;
 
-        #if 1
         /* HACK! Treat META as ALT; see bug #663779. */
         if (mods & GDK_META_MASK)
                 mods = GdkModifierType(mods | GDK_MOD1_MASK);
-        #endif
 
         /* Map non-virtual modifiers to virtual modifiers (Super, Hyper, Meta) */
         auto display = gdk_window_get_display(gdk_event_get_window(event));
@@ -390,9 +597,12 @@ Widget::read_modifiers_from_gdk(GdkEvent* event) const noexcept
         return unsigned(mods);
 }
 
+#endif /* VTE_GTK == 3 */
+
 unsigned
 Widget::key_event_translate_ctrlkey(KeyEvent const& event) const noexcept
 {
+#if VTE_GTK == 3
        if (event.keyval() < 128)
                return event.keyval();
 
@@ -417,33 +627,55 @@ Widget::key_event_translate_ctrlkey(KeyEvent const& event) const noexcept
        }
 
         return keyval;
+#elif VTE_GTK == 4
+        // FIXMEgtk4: find a way to do this on gtk4
+        return event.keyval();
+#endif
 }
 
 KeyEvent
-Widget::key_event_from_gdk(GdkEventKey* event) const
+Widget::key_event_from_gdk(GdkEvent* event) const
 {
         auto type = EventBase::Type{};
-        switch (gdk_event_get_event_type(reinterpret_cast<GdkEvent*>(event))) {
+        switch (gdk_event_get_event_type(event)) {
         case GDK_KEY_PRESS: type = KeyEvent::Type::eKEY_PRESS;     break;
         case GDK_KEY_RELEASE: type = KeyEvent::Type::eKEY_RELEASE; break;
         default: g_assert_not_reached(); return {};
         }
 
-        auto base_event = reinterpret_cast<GdkEvent*>(event);
-        return {base_event,
+#if VTE_GTK == 3
+        auto keyval = unsigned{};
+        gdk_event_get_keyval(event, &keyval);
+        auto const scancode = unsigned(reinterpret_cast<GdkEventKey*>(event)->hardware_keycode);
+        auto const group = reinterpret_cast<GdkEventKey*>(event)->group;
+        auto const is_modifier = reinterpret_cast<GdkEventKey*>(event)->is_modifier != 0;
+#elif VTE_GTK == 4
+        auto keyval = gdk_key_event_get_keyval(event);
+        auto scancode = gdk_key_event_get_keycode(event);
+        auto const group = gdk_key_event_get_level(event);
+        auto const is_modifier = gdk_key_event_is_modifier(event) != false;
+#endif /* VTE_GTK */
+
+        return {event,
                 type,
-                read_modifiers_from_gdk(base_event),
-                event->keyval,
-                event->hardware_keycode, // gdk_event_get_scancode(event),
-                event->group,
-                event->is_modifier != 0};
+#if VTE_GTK == 3
+                read_modifiers_from_gdk(event),
+#elif VTE_GTK == 4
+                gdk_event_get_modifier_state(event),
+#endif
+                keyval,
+                scancode,
+                group,
+                is_modifier};
 }
 
+#if VTE_GTK == 3
+
 MouseEvent
 Widget::mouse_event_from_gdk(GdkEvent* event) const /* throws */
 {
         auto type = EventBase::Type{};
-        auto press_count = 0u;
+        auto press_count = 0;
         switch (gdk_event_get_event_type(event)) {
         case GDK_2BUTTON_PRESS:
                 type = MouseEvent::Type::eMOUSE_PRESS;
@@ -478,7 +710,7 @@ Widget::mouse_event_from_gdk(GdkEvent* event) const /* throws */
             !gdk_event_get_coords(event, &x, &y))
                 x = y = -1.; // FIXMEchpe or throw?
 
-        auto button = unsigned{0};
+        auto button = 0u;
         (void)gdk_event_get_button(event, &button);
 
         return {type,
@@ -512,24 +744,79 @@ Widget::scroll_event_from_gdk(GdkEvent* event) const /* throws */
                 dx, dy};
 }
 
+#endif /* VTE_GTK == 3 */
+
 void
 Widget::map() noexcept
 {
+#if VTE_GTK == 3
         if (m_event_window)
                 gdk_window_show_unraised(m_event_window);
+#endif
+}
+
+#if VTE_GTK == 4
+
+void
+Widget::measure(GtkOrientation orientation,
+                int for_size,
+                int* minimum,
+                int* natural,
+                int* minimum_baseline,
+                int* natural_baseline)
+{
+        _vte_debug_print(VTE_DEBUG_WIDGET_SIZE, "Widget measure for_size=%d orientation=%s\n",
+                         for_size,
+                         orientation == GTK_ORIENTATION_HORIZONTAL ? "horizontal" : "vertical");
+
+        switch (orientation) {
+        case GTK_ORIENTATION_HORIZONTAL:
+                terminal()->widget_measure_width(minimum, natural);
+                break;
+        case GTK_ORIENTATION_VERTICAL:
+                *minimum_baseline = *natural_baseline = 0;
+                terminal()->widget_measure_height(minimum, natural);
+                break;
+        }
+}
+
+#endif /* VTE_GTK == 4 */
+
+void
+Widget::padding_changed()
+{
+#if VTE_GTK == 3
+        auto padding = GtkBorder{};
+        auto context = gtk_widget_get_style_context(gtk());
+        gtk_style_context_get_padding(context,
+#if VTE_GTK == 3
+                                      gtk_style_context_get_state(context),
+#endif
+                                      &padding);
+        terminal()->set_border_padding(&padding);
+#endif /* VTE_GTK FIXMEgtk4 how to handle margin/padding? */
 }
 
 bool
 Widget::primary_paste_enabled() const noexcept
 {
         auto primary_paste = gboolean{};
-        g_object_get(gtk_widget_get_settings(gtk()),
+        g_object_get(m_settings.get(),
                      "gtk-enable-primary-paste", &primary_paste,
                      nullptr);
 
         return primary_paste != false;
 }
 
+bool
+Widget::query_tooltip(int x,
+                      int y,
+                      bool keyboard,
+                      GtkTooltip* tooltip) noexcept
+{
+        return false;
+}
+
 void
 Widget::realize() noexcept
 {
@@ -545,6 +832,7 @@ Widget::realize() noexcept
         else
                 m_hyperlink_cursor = create_cursor(VTE_HYPERLINK_CURSOR);
 
+#if VTE_GTK == 3
        /* Create an input window for the widget. */
         auto allocation = m_terminal->get_allocated_rect();
        GdkWindowAttr attributes;
@@ -579,15 +867,21 @@ Widget::realize() noexcept
        m_event_window = gdk_window_new(gtk_widget_get_parent_window (m_widget),
                                         &attributes, attributes_mask);
         gtk_widget_register_window(m_widget, m_event_window);
+#endif /* VTE_GTK == 3 */
 
         assert(!m_im_context);
-       m_im_context.reset(gtk_im_multicontext_new());
-#if GTK_CHECK_VERSION (3, 24, 14)
+        m_im_context = vte::glib::take_ref(gtk_im_multicontext_new());
+#if (VTE_GTK == 3 && GTK_CHECK_VERSION (3, 24, 14)) || VTE_GTK == 4
         g_object_set(m_im_context.get(),
                      "input-purpose", GTK_INPUT_PURPOSE_TERMINAL,
                      nullptr);
 #endif
+
+#if VTE_GTK == 3
        gtk_im_context_set_client_window(m_im_context.get(), m_event_window);
+#elif VTE_GTK == 4
+        gtk_im_context_set_client_widget(m_im_context.get(), gtk());
+#endif
        g_signal_connect(m_im_context.get(), "commit",
                         G_CALLBACK(im_commit_cb), this);
        g_signal_connect(m_im_context.get(), "preedit-start",
@@ -608,42 +902,82 @@ Widget::realize() noexcept
         m_terminal->widget_realize();
 }
 
+#if VTE_GTK == 4
+
+void
+Widget::root()
+{
+}
+
+#endif /* VTE_GTK == 4 */
+
+#if VTE_GTK == 3
+
 void
 Widget::screen_changed(GdkScreen *previous_screen) noexcept
 {
         auto gdk_screen = gtk_widget_get_screen(m_widget);
-        if (previous_screen != nullptr &&
-            (gdk_screen != previous_screen || gdk_screen == nullptr)) {
-                auto settings = gtk_settings_get_for_screen(previous_screen);
-                g_signal_handlers_disconnect_matched(settings, G_SIGNAL_MATCH_DATA,
+        if (gdk_screen == previous_screen || gdk_screen == nullptr)
+                return;
+
+        connect_settings();
+}
+
+#elif VTE_GTK == 4
+
+void
+Widget::display_changed() noexcept
+{
+        /* There appears to be no way to retrieve the previous display */
+        connect_settings();
+}
+
+#endif /* VTE_GTK */
+
+void
+Widget::connect_settings()
+{
+        auto settings = vte::glib::make_ref(gtk_widget_get_settings(m_widget));
+        if (settings == m_settings)
+                return;
+
+        if (m_settings)
+                g_signal_handlers_disconnect_matched(m_settings.get(), G_SIGNAL_MATCH_DATA,
                                                      0, 0, nullptr, nullptr,
                                                      this);
-        }
 
-        if (gdk_screen == previous_screen || gdk_screen == nullptr)
-                return;
+        m_settings = std::move(settings);
 
         settings_changed();
 
-        auto settings = gtk_widget_get_settings(m_widget);
-        g_signal_connect (settings, "notify::gtk-cursor-blink",
-                          G_CALLBACK(settings_notify_cb), this);
-        g_signal_connect (settings, "notify::gtk-cursor-blink-time",
-                          G_CALLBACK(settings_notify_cb), this);
-        g_signal_connect (settings, "notify::gtk-cursor-blink-timeout",
-                          G_CALLBACK(settings_notify_cb), this);
+        g_signal_connect(m_settings.get(), "notify::gtk-cursor-blink",
+                         G_CALLBACK(settings_notify_cb), this);
+        g_signal_connect(m_settings.get(), "notify::gtk-cursor-blink-time",
+                         G_CALLBACK(settings_notify_cb), this);
+        g_signal_connect(m_settings.get(), "notify::gtk-cursor-blink-timeout",
+                         G_CALLBACK(settings_notify_cb), this);
+#if VTE_GTK == 4
+        g_signal_connect(m_settings.get(), "notify::gtk-cursor-aspect-ratio",
+                         G_CALLBACK(settings_notify_cb), this);
+#endif
 }
 
 void
-Widget::settings_changed() noexcept
+Widget::settings_changed()
 {
         auto blink = gboolean{};
         auto blink_time = int{};
         auto blink_timeout = int{};
-        g_object_get(gtk_widget_get_settings(m_widget),
+#if VTE_GTK == 4
+        auto aspect = double{};
+#endif
+        g_object_get(m_settings.get(),
                      "gtk-cursor-blink", &blink,
                      "gtk-cursor-blink-time", &blink_time,
                      "gtk-cursor-blink-timeout", &blink_timeout,
+#if VTE_GTK == 4
+                     "gtk-cursor-aspect-ratio", &aspect,
+#endif
                      nullptr);
 
         _vte_debug_print(VTE_DEBUG_MISC,
@@ -651,6 +985,10 @@ Widget::settings_changed() noexcept
                          blink, blink_time, blink_timeout);
 
         m_terminal->set_blink_settings(blink, blink_time, blink_timeout);
+
+#if VTE_GTK == 4
+        m_terminal->set_cursor_aspect(aspect);
+#endif
 }
 
 void
@@ -664,6 +1002,30 @@ Widget::set_cursor(CursorType type) noexcept
         }
 }
 
+void
+Widget::set_hscroll_policy(GtkScrollablePolicy policy)
+{
+        m_hscroll_policy = policy;
+
+#if VTE_GTK == 3
+        gtk_widget_queue_resize_no_redraw(gtk());
+#elif VTE_GTK == 4
+        gtk_widget_queue_resize(gtk());
+#endif
+}
+
+void
+Widget::set_vscroll_policy(GtkScrollablePolicy policy)
+{
+        m_vscroll_policy = policy;
+
+#if VTE_GTK == 3
+        gtk_widget_queue_resize_no_redraw(gtk());
+#elif VTE_GTK == 4
+        gtk_widget_queue_resize(gtk());
+#endif
+}
+
 bool
 Widget::set_pty(VtePty* pty_obj) noexcept
 {
@@ -704,10 +1066,19 @@ Widget::unset_pty() noexcept
         g_object_notify_by_pspec(object(), pspecs[PROP_PTY]);
 }
 
+#if VTE_GTK == 3
+
 void
-Widget::size_allocate(GtkAllocation* allocation) noexcept
+Widget::size_allocate(GtkAllocation* allocation)
 {
-        m_terminal->widget_size_allocate(allocation);
+        _vte_debug_print(VTE_DEBUG_WIDGET_SIZE, "Widget size allocate width=%d height=%d x=%d y=%d\n",
+                         allocation->width, allocation->height, allocation->x, allocation->y);
+
+        m_terminal->widget_size_allocate(allocation->x, allocation->y,
+                                         allocation->width, allocation->height,
+                                         -1);
+
+        gtk_widget_set_allocation(gtk(), allocation);
 
         if (realized())
                gdk_window_move_resize(m_event_window,
@@ -717,6 +1088,23 @@ Widget::size_allocate(GtkAllocation* allocation) noexcept
                                        allocation->height);
 }
 
+#elif VTE_GTK == 4
+
+void
+Widget::size_allocate(int width,
+                      int height,
+                      int baseline)
+{
+        _vte_debug_print(VTE_DEBUG_WIDGET_SIZE, "Widget size allocate width=%d height=%d baseline=%d\n",
+                         width, height, baseline);
+
+        terminal()->widget_size_allocate(width, height, baseline);
+
+        gtk_widget_allocate(gtk(), width, height, baseline, nullptr);
+}
+
+#endif /* VTE_GTK */
+
 bool
 Widget::should_emit_signal(int id) noexcept
 {
@@ -726,14 +1114,36 @@ Widget::should_emit_signal(int id) noexcept
                                             false /* not interested in blocked handlers */) != FALSE;
 }
 
+void
+Widget::state_flags_changed(GtkStateFlags old_flags)
+{
+        _vte_debug_print(VTE_DEBUG_STYLE, "Widget state flags changed\n");
+}
+
+#if VTE_GTK == 4
+
+void
+Widget::snapshot(GtkSnapshot* snapshot_object)
+{
+        _vte_debug_print(VTE_DEBUG_DRAW, "Widget snapshot\n");
+
+        auto rect = terminal()->allocated_rect();
+        auto region = vte::take_freeable(cairo_region_create_rectangle(rect));
+        auto grect = vte::graphene::make_rect(rect);
+        auto cr = vte::take_freeable(gtk_snapshot_append_cairo(snapshot_object, &grect));
+        terminal()->draw(cr.get(), region.get());
+}
+
+#endif /* VTE_GTK == 4 */
+
+#if VTE_GTK == 3
+
 void
 Widget::style_updated() noexcept
 {
-        auto padding = GtkBorder{};
-        auto context = gtk_widget_get_style_context(gtk());
-        gtk_style_context_get_padding(context, gtk_style_context_get_state(context),
-                                      &padding);
-        m_terminal->set_border_padding(&padding);
+        _vte_debug_print(VTE_DEBUG_STYLE, "Widget style changed\n");
+
+        padding_changed();
 
         auto aspect = float{};
         gtk_widget_style_get(gtk(), "cursor-aspect-ratio", &aspect, nullptr);
@@ -742,13 +1152,48 @@ Widget::style_updated() noexcept
         m_terminal->widget_style_updated();
 }
 
+#endif /* VTE_GTK == 3 */
+
+#if VTE_GTK == 4
+
+void
+Widget::system_setting_changed(GtkSystemSetting setting)
+{
+        _vte_debug_print(VTE_DEBUG_STYLE, "Widget system settings %d changed\n", int(setting));
+
+        switch (setting) {
+        case GTK_SYSTEM_SETTING_DISPLAY:
+                display_changed();
+                break;
+
+        case GTK_SYSTEM_SETTING_DPI:
+                break;
+
+        case GTK_SYSTEM_SETTING_FONT_CONFIG:
+                break;
+
+        case GTK_SYSTEM_SETTING_FONT_NAME:
+                break;
+
+        case GTK_SYSTEM_SETTING_ICON_THEME:
+                break;
+
+        default:
+                break;
+        }
+}
+
+#endif /* VTE_GTK == 4 */
+
 void
 Widget::unmap() noexcept
 {
         m_terminal->widget_unmap();
 
+#if VTE_GTK == 3
         if (m_event_window)
                 gdk_window_hide(m_event_window);
+#endif
 }
 
 void
@@ -756,16 +1201,17 @@ Widget::unrealize() noexcept
 {
         m_terminal->widget_unrealize();
 
+        // FIXMEgtk4 only withdraw content from clipboard, not unselect?
         if (m_clipboard) {
                 terminal()->widget_clipboard_data_clear(*m_clipboard);
                 m_clipboard->disown();
+                m_clipboard.reset();
         }
         if (m_primary_clipboard) {
                 terminal()->widget_clipboard_data_clear(*m_primary_clipboard);
                 m_primary_clipboard->disown();
+                m_primary_clipboard.reset();
         }
-        m_clipboard.reset();
-        m_primary_clipboard.reset();
 
         m_default_cursor.reset();
         m_invisible_cursor.reset();
@@ -779,15 +1225,30 @@ Widget::unrealize() noexcept
                                              0, 0, NULL, NULL,
                                              this);
         m_terminal->im_preedit_reset();
+#if VTE_GTK == 3
         gtk_im_context_set_client_window(m_im_context.get(), nullptr);
+#elif VTE_GTK == 4
+        gtk_im_context_set_client_widget(m_im_context.get(), nullptr);
+#endif
         m_im_context.reset();
 
+#if VTE_GTK == 3
         /* Destroy input window */
         gtk_widget_unregister_window(m_widget, m_event_window);
         gdk_window_destroy(m_event_window);
         m_event_window = nullptr;
+#endif /* VTE_GTK == 3 */
 }
 
+#if VTE_GTK == 4
+
+void
+Widget::unroot()
+{
+}
+
+#endif /* VTE_GTK == 4 */
+
 } // namespace platform
 
 } // namespace vte
diff --git a/src/widget.hh b/src/widget.hh
index ede09be3..813b3625 100644
--- a/src/widget.hh
+++ b/src/widget.hh
@@ -20,6 +20,8 @@
 #include <memory>
 #include <optional>
 #include <string>
+#include <tuple>
+#include <utility>
 #include <variant>
 
 #include "vteterminal.h"
@@ -95,7 +97,7 @@ protected:
                            unsigned modifiers,
                            unsigned keyval,
                            unsigned keycode,
-                           uint8_t group,
+                           unsigned group,
                            bool is_modifier) noexcept
                 : EventBase{type},
                   m_platform_event{gdk_event},
@@ -126,12 +128,23 @@ public:
         constexpr auto is_key_press()   const noexcept { return type() == Type::eKEY_PRESS;   }
         constexpr auto is_key_release() const noexcept { return type() == Type::eKEY_RELEASE; }
 
+        bool matches(unsigned keyval,
+                     unsigned modifiers) const noexcept
+        {
+#if VTE_GTK == 3
+                return false; // FIXMEgtk3
+#elif VTE_GTK == 4
+                return gdk_key_event_matches(platform_event(),
+                                             keyval, GdkModifierType(modifiers)) == GDK_KEY_MATCH_EXACT;
+#endif
+        }
+
 private:
         GdkEvent* m_platform_event;
         unsigned m_modifiers;
         unsigned m_keyval;
         unsigned m_keycode;
-        uint8_t m_group;
+        unsigned m_group;
         bool m_is_modifier;
 }; // class KeyEvent
 
@@ -154,7 +167,7 @@ protected:
         MouseEvent() noexcept = default;
 
         constexpr MouseEvent(Type type,
-                             unsigned press_count,
+                             int press_count,
                              unsigned modifiers,
                              Button button,
                              double x,
@@ -190,7 +203,7 @@ public:
         constexpr auto is_mouse_release()      const noexcept { return type() == Type::eMOUSE_RELEASE;      }
 
 private:
-        unsigned m_press_count;
+        int m_press_count;
         unsigned m_modifiers;
         Button m_button;
         double m_x;
@@ -257,24 +270,60 @@ public:
         void unrealize() noexcept;
         void map() noexcept;
         void unmap() noexcept;
+        void state_flags_changed(GtkStateFlags old_flags);
+        void direction_changed(GtkTextDirection old_direction) noexcept;
+        bool query_tooltip(int x,
+                           int y,
+                           bool keyboard,
+                           GtkTooltip* tooltip) noexcept;
+
+        void connect_settings();
+        void padding_changed();
+        void settings_changed();
+
+#if VTE_GTK == 3
         void style_updated() noexcept;
         void draw(cairo_t *cr) noexcept { m_terminal->widget_draw(cr); }
         void get_preferred_width(int *minimum_width,
-                                 int *natural_width) const noexcept { 
m_terminal->widget_get_preferred_width(minimum_width, natural_width); }
+                                 int *natural_width) const noexcept { 
m_terminal->widget_measure_width(minimum_width, natural_width); }
         void get_preferred_height(int *minimum_height,
-                                  int *natural_height) const noexcept { 
m_terminal->widget_get_preferred_height(minimum_height, natural_height); }
-        void size_allocate(GtkAllocation *allocation) noexcept;
-
-        void focus_in(GdkEventFocus *event) noexcept { m_terminal->widget_focus_in(); }
-        void focus_out(GdkEventFocus *event) noexcept { m_terminal->widget_focus_out(); }
-        bool key_press(GdkEventKey *event) noexcept { return 
m_terminal->widget_key_press(key_event_from_gdk(event)); }
-        bool key_release(GdkEventKey *event) noexcept { return 
m_terminal->widget_key_release(key_event_from_gdk(event)); }
-        bool button_press(GdkEventButton *event) noexcept { return 
m_terminal->widget_mouse_press(mouse_event_from_gdk(reinterpret_cast<GdkEvent*>(event))); }
-        bool button_release(GdkEventButton *event) noexcept { return 
m_terminal->widget_mouse_release(mouse_event_from_gdk(reinterpret_cast<GdkEvent*>(event))); }
-        void enter(GdkEventCrossing *event) noexcept { 
m_terminal->widget_mouse_enter(mouse_event_from_gdk(reinterpret_cast<GdkEvent*>(event))); }
-        void leave(GdkEventCrossing *event) noexcept { 
m_terminal->widget_mouse_leave(mouse_event_from_gdk(reinterpret_cast<GdkEvent*>(event))); }
-        bool scroll(GdkEventScroll *event) noexcept { return 
m_terminal->widget_mouse_scroll(scroll_event_from_gdk(reinterpret_cast<GdkEvent*>(event))); }
-        bool motion_notify(GdkEventMotion *event) noexcept { return 
m_terminal->widget_mouse_motion(mouse_event_from_gdk(reinterpret_cast<GdkEvent*>(event))); }
+                                  int *natural_height) const noexcept { 
m_terminal->widget_measure_height(minimum_height, natural_height); }
+        void size_allocate(GtkAllocation *allocation);
+
+        void event_focus_in(GdkEventFocus *event);
+        void event_focus_out(GdkEventFocus *event);
+        bool event_key_press(GdkEventKey *event);
+        bool event_key_release(GdkEventKey *event);
+        bool event_button_press(GdkEventButton *event);
+        bool event_button_release(GdkEventButton *event);
+        void event_enter(GdkEventCrossing *event);
+        void event_leave(GdkEventCrossing *event);
+        bool event_scroll(GdkEventScroll *event);
+        bool event_motion_notify(GdkEventMotion *event);
+
+        void screen_changed (GdkScreen *previous_screen) noexcept;
+#endif /* VTE_GTK == 3 */
+
+#if VTE_GTK == 4
+        void size_allocate(int width,
+                           int height,
+                           int baseline);
+        void root();
+        void unroot();
+        void measure(GtkOrientation orientation,
+                     int for_size,
+                     int* minimum,
+                     int* natural,
+                     int* minimum_baseline,
+                     int* natural_baseline);
+        std::pair<bool, bool> compute_expand();
+        void css_changed(GtkCssStyleChange* change);
+        void system_setting_changed(GtkSystemSetting setting);
+        void snapshot(GtkSnapshot* snapshot_object);
+        bool contains(double x,
+                      double y);
+        void display_changed() noexcept;
+#endif /* VTE_GTK == 4 */
 
         void grab_focus() noexcept { gtk_widget_grab_focus(gtk()); }
 
@@ -291,17 +340,14 @@ public:
         void copy(vte::platform::ClipboardType type,
                   vte::platform::ClipboardFormat format) noexcept { m_terminal->widget_copy(type, format); }
 
-        void screen_changed (GdkScreen *previous_screen) noexcept;
-        void settings_changed() noexcept;
-
         void beep() noexcept;
 
-        void set_hadjustment(vte::glib::RefPtr<GtkAdjustment>&& adjustment) noexcept { m_hadjustment = 
std::move(adjustment); }
-        void set_vadjustment(vte::glib::RefPtr<GtkAdjustment>&& adjustment) { 
terminal()->widget_set_vadjustment(std::move(adjustment)); }
+        void set_hadjustment(vte::glib::RefPtr<GtkAdjustment> adjustment) noexcept { m_hadjustment = 
std::move(adjustment); }
+        void set_vadjustment(vte::glib::RefPtr<GtkAdjustment> adjustment) { 
terminal()->widget_set_vadjustment(std::move(adjustment)); }
         auto hadjustment() noexcept { return m_hadjustment.get(); }
         auto vadjustment() noexcept { return terminal()->vadjustment(); }
-        void set_hscroll_policy(GtkScrollablePolicy policy) noexcept { m_hscroll_policy = policy; }
-        void set_vscroll_policy(GtkScrollablePolicy policy) noexcept { m_vscroll_policy = policy; }
+        void set_hscroll_policy(GtkScrollablePolicy policy);
+        void set_vscroll_policy(GtkScrollablePolicy policy);
         auto hscroll_policy() const noexcept { return m_hscroll_policy; }
         auto vscroll_policy() const noexcept { return m_vscroll_policy; }
         auto padding() const noexcept { return terminal()->padding(); }
@@ -346,12 +392,15 @@ public:
                 return terminal()->regex_match_check(column, row, tag);
         }
 
+#if VTE_GTK == 3
+
         char* regex_match_check(GdkEvent* event,
                                 int* tag)
         {
                 return terminal()->regex_match_check(mouse_event_from_gdk(event), tag);
         }
 
+
         bool regex_match_check_extra(GdkEvent* event,
                                      vte::base::Regex const** regexes,
                                      size_t n_regexes,
@@ -367,6 +416,8 @@ public:
                 return terminal()->hyperlink_check(mouse_event_from_gdk(event));
         }
 
+#endif /* VTE_GTK */
+
         bool should_emit_signal(int id) noexcept;
 
         bool set_sixel_enabled(bool enabled) noexcept { return m_terminal->set_sixel_enabled(enabled); }
@@ -381,7 +432,9 @@ protected:
                 eHyperlink
         };
 
+#if VTE_GTK == 3
         GdkWindow* event_window() const noexcept { return m_event_window; }
+#endif
 
         bool realized() const noexcept
         {
@@ -415,10 +468,12 @@ public: // FIXMEchpe
         void im_preedit_changed() noexcept;
 
 private:
+        KeyEvent key_event_from_gdk(GdkEvent* event) const;
+#if VTE_GTK == 3
         unsigned read_modifiers_from_gdk(GdkEvent* event) const noexcept;
-        KeyEvent key_event_from_gdk(GdkEventKey* event) const;
         MouseEvent mouse_event_from_gdk(GdkEvent* event) const /* throws */;
         ScrollEvent scroll_event_from_gdk(GdkEvent* event) const /* throws */;
+#endif
 
         void clipboard_request_received_cb(Clipboard const& clipboard,
                                            std::string_view const& text);
@@ -432,8 +487,12 @@ private:
 
         vte::terminal::Terminal* m_terminal;
 
+#if VTE_GTK == 3
         /* Event window */
         GdkWindow *m_event_window;
+#endif
+
+        vte::glib::RefPtr<GtkSettings> m_settings{nullptr};
 
         /* Cursors */
         vte::glib::RefPtr<GdkCursor> m_default_cursor;


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]