patch 9.2.0632: GTK4: no support for hardware-accelerated rendering

Commit: 
https://github.com/vim/vim/commit/c1e0c8edf2402dd2ffb3e261a3d67356f73e6467
Author: Foxe Chen <[email protected]>
Date:   Sat Jun 13 17:49:03 2026 +0000

    patch 9.2.0632: GTK4: no support for hardware-accelerated rendering
    
    Problem:  The GTK4 GUI renders via a Cairo backing surface, which may be
              slow for large windows and high-resolution displays.
    Solution: Add the --enable-gtk4-hwaccel configure option, which
              switches the GTK4 GUI to GtkSnapshot-based rendering.  Popup
              images use the new GDK image backend which uploads textures to
              the GPU (Foxe Chen).
    
    closes: #20437
    
    Signed-off-by: Foxe Chen <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

diff --git a/Filelist b/Filelist
index 33e07d5d3..5526e539a 100644
--- a/Filelist
+++ b/Filelist
@@ -513,6 +513,8 @@ SRC_UNIX =  \
                src/gui_gtk4_f.h \
                src/gui_gtk4_cb.c \
                src/gui_gtk4_cb.h \
+               src/gui_gtk4_da.c \
+               src/gui_gtk4_da.h \
                src/gui_gtk_res.xml \
                src/gui_motif.c \
                src/gui_xmdlg.c \
diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index caad78ceb..06c258eaf 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -13310,8 +13310,9 @@ hurd                    GNU/Hurd version of Vim
 iconv                  Can use iconv() for conversion.
 image                  Compiled with the popup window "image" attribute.
                        See |popup-image|.
-image_cairo            Compiled with the Cairo image backend (GTK GUI).
+image_cairo            Compiled with the Cairo image backend (GTK2 and GTK3 
GUI).
 image_gdi              Compiled with the GDI image backend (Windows GUI).
+image_gdk              Compiled with the GDK image backend (GTK4 GUI).
 image_kitty            Compiled with the kitty graphics protocol image backend
                        (terminal).
 image_sixel            Compiled with the DEC sixel image backend (terminal).
diff --git a/runtime/doc/gui_x11.txt b/runtime/doc/gui_x11.txt
index 5f8056beb..2e558d8a0 100644
--- a/runtime/doc/gui_x11.txt
+++ b/runtime/doc/gui_x11.txt
@@ -1,4 +1,4 @@
-*gui_x11.txt*  For Vim version 9.2.  Last change: 2026 Jun 07
+*gui_x11.txt*  For Vim version 9.2.  Last change: 2026 Jun 13
 
 
                  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -366,7 +366,14 @@ to use software rendering: >
 
     $ GSK_RENDERER=cairo gvim
     $ LIBGL_ALWAYS_SOFTWARE=true gvim
-
+<
+                                                       *gtk4-hwaccel*
+If Vim is compiled with the configure option "--enable-gtk4-hwaccel" set, then
+the GTK4 GUI will use GtkSnapshot instead of Cairo, allowing for hardware
+accelerated rendering, which is much faster.  Enabling this configure option
+also makes the GTK4 GUI use |+image_gdk| instead of |+image_cairo| for
+rendering popup window images.  Note that this feature is currently
+experimental.
 
 Tooltip Colors ~
                                                        *gtk-tooltip-colors*
diff --git a/runtime/doc/popup.txt b/runtime/doc/popup.txt
index a06924b99..cf4e9e254 100644
--- a/runtime/doc/popup.txt
+++ b/runtime/doc/popup.txt
@@ -1,4 +1,4 @@
-*popup.txt*    For Vim version 9.2.  Last change: 2026 Jun 09
+*popup.txt*    For Vim version 9.2.  Last change: 2026 Jun 13
 
 
                  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -1186,6 +1186,8 @@ whichever backend is available at runtime:
            |+image_gdi|.
     Cairo   composite onto a cairo_image_surface_t on the GTK GUI
            (covers GTK 2 and GTK 3).  |+image_cairo|.
+    GDK            Use GdkTexture, which uploads the image onto the GPU for 
faster
+           rendering (only for GTK 4). |+image_gdk|
 
 Vim itself does NOT link against libpng, libjpeg, libwebp or any image
 decoder.  Format decoding is left to the caller, who can pipe the file
diff --git a/runtime/doc/tags b/runtime/doc/tags
index 086af1973..88fad5440 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -1473,6 +1473,7 @@ $quote    eval.txt        /*$quote*
 +image various.txt     /*+image*
 +image_cairo   various.txt     /*+image_cairo*
 +image_gdi     various.txt     /*+image_gdi*
++image_gdk     various.txt     /*+image_gdk*
 +image_kitty   various.txt     /*+image_kitty*
 +image_sixel   various.txt     /*+image_sixel*
 +insert_expand various.txt     /*+insert_expand*
@@ -8254,6 +8255,7 @@ gt        tabpage.txt     /*gt*
 gtk-css        gui_x11.txt     /*gtk-css*
 gtk-tooltip-colors     gui_x11.txt     /*gtk-tooltip-colors*
 gtk3-slow      gui_x11.txt     /*gtk3-slow*
+gtk4-hwaccel   gui_x11.txt     /*gtk4-hwaccel*
 gtk4-slow      gui_x11.txt     /*gtk4-slow*
 gu     change.txt      /*gu*
 gugu   change.txt      /*gugu*
diff --git a/runtime/doc/various.txt b/runtime/doc/various.txt
index cc3ef1075..4a4bd6955 100644
--- a/runtime/doc/various.txt
+++ b/runtime/doc/various.txt
@@ -1,4 +1,4 @@
-*various.txt*  For Vim version 9.2.  Last change: 2026 Jun 09
+*various.txt*  For Vim version 9.2.  Last change: 2026 Jun 13
 
 
                  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -424,8 +424,9 @@ m  *+hangul_input*  Hangul input support |hangul|
    *+iconv*            Compiled with the |iconv()| function
    *+iconv/dyn*                Likewise |iconv-dynamic| |/dyn|
 H  *+image*            popup window image attribute, see |popup-image|
-H  *+image_cairo*      |+image| Cairo backend (GTK GUI)
+H  *+image_cairo*      |+image| Cairo backend (GTK2 and GTK3 GUI)
 H  *+image_gdi*                |+image| GDI backend (Windows GUI)
+H  *+image_gdk*                |+image| GDK backend (GTK4 GUI)
 H  *+image_kitty*      |+image| kitty graphics protocol backend (terminal)
 H  *+image_sixel*      |+image| DEC sixel backend (terminal)
 T  *+insert_expand*    |insert_expand| Insert mode completion
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index b78a3738a..df216fae1 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -52655,6 +52655,7 @@ Platform specific ~
 -----------------
 - support OpenType font features in 'guifont' for DirectWrite (Win32)
 - Include strptime() fallback for MS-Windows
+- Hardware-accelerated rendering for the GTK4 GUI via |gtk4-hwaccel|.
 
 xxd ~
 ---
diff --git a/src/Makefile b/src/Makefile
index e8b8be8e5..1a6fa1c66 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -1235,10 +1235,12 @@ GTK_BUNDLE      =
 ### GTK4 GUI
 GTK4_SRC       = gui.c gui_gtk4.c gui_gtk4_f.c \
                        gui_gtk4_cb.c \
+                       gui_gtk4_da.c \
                        $(GRESOURCE_SRC)
 GTK4_OBJ       = objects/gui.o objects/gui_gtk4.o \
                        objects/gui_gtk4_f.o \
                        objects/gui_gtk4_cb.o \
+                       objects/gui_gtk4_da.o \
                        $(GRESOURCE_OBJ)
 GTK4_DEFS      = -DFEAT_GUI_GTK $(NARROW_PROTO)
 GTK4_IPATH     = $(GUI_INC_LOC)
@@ -1308,7 +1310,7 @@ HAIKUGUI_TESTTARGET = gui
 HAIKUGUI_BUNDLE =
 
 # All GUI files
-ALL_GUI_SRC  = gui.c gui_gtk.c gui_gtk_f.c gui_gtk4.c gui_gtk4_f.c 
gui_gtk4_cb.c gui_motif.c gui_xmdlg.c gui_xmebw.c gui_gtk_x11.c gui_x11.c 
gui_haiku.cc
+ALL_GUI_SRC  = gui.c gui_gtk.c gui_gtk_f.c gui_gtk4.c gui_gtk4_f.c 
gui_gtk4_cb.c gui_gtk4_da.c gui_motif.c gui_xmdlg.c gui_xmebw.c gui_gtk_x11.c 
gui_x11.c gui_haiku.cc
 ALL_GUI_PRO  = proto/gui.pro proto/gui_gtk.pro proto/gui_gtk4.pro 
proto/gui_motif.pro proto/gui_xmdlg.pro proto/gui_gtk_x11.pro proto/gui_x11.pro 
proto/gui_w32.pro proto/gui_photon.pro
 
 # }}}
@@ -3403,6 +3405,9 @@ objects/gui_gtk4_f.o: gui_gtk4_f.c
 objects/gui_gtk4_cb.o: gui_gtk4_cb.c
        $(CCC) -o $@ gui_gtk4_cb.c
 
+objects/gui_gtk4_da.o: gui_gtk4_da.c
+       $(CCC) -o $@ gui_gtk4_da.c
+
 
 objects/gui_haiku.o: gui_haiku.cc
        $(CCC) -o $@ gui_haiku.cc
@@ -4499,7 +4504,8 @@ objects/gui_gtk4.o: auto/osdef.h gui_gtk4.c vim.h 
protodef.h auto/config.h featu
  ascii.h keymap.h termdefs.h macros.h option.h beval.h \
  structs.h regexp.h gui.h libvterm/include/vterm.h \
  libvterm/include/vterm_keycodes.h alloc.h ex_cmds.h spell.h proto.h \
- globals.h errors.h gui_gtk4_f.h auto/gui_gtk_gresources.h
+ globals.h errors.h gui_gtk4_f.h auto/gui_gtk_gresources.h \
+ gui_gtk4_da.h
 objects/gui_gtk4_f.o: auto/osdef.h gui_gtk4_f.c vim.h protodef.h auto/config.h 
feature.h \
  os_unix.h ascii.h keymap.h termdefs.h macros.h option.h \
  beval.h structs.h regexp.h gui.h \
@@ -4510,6 +4516,11 @@ objects/gui_gtk4_cb.o: auto/osdef.h gui_gtk4_cb.c vim.h 
protodef.h auto/config.h
  beval.h structs.h regexp.h gui.h \
  libvterm/include/vterm.h libvterm/include/vterm_keycodes.h alloc.h \
  ex_cmds.h spell.h proto.h globals.h errors.h gui_gtk4_cb.h
+objects/gui_gtk4_da.o: auto/osdef.h gui_gtk4_da.c vim.h protodef.h 
auto/config.h feature.h \
+ os_unix.h ascii.h keymap.h termdefs.h macros.h option.h \
+ beval.h structs.h regexp.h gui.h \
+ libvterm/include/vterm.h libvterm/include/vterm_keycodes.h alloc.h \
+ ex_cmds.h spell.h proto.h globals.h errors.h gui_gtk4_da.h
 objects/gui_gtk_f.o: auto/osdef.h gui_gtk_f.c vim.h protodef.h auto/config.h 
feature.h \
  os_unix.h ascii.h keymap.h termdefs.h macros.h option.h \
  beval.h structs.h regexp.h gui.h \
diff --git a/src/auto/configure b/src/auto/configure
index 17813c5db..483beff82 100755
--- a/src/auto/configure
+++ b/src/auto/configure
@@ -862,6 +862,7 @@ enable_gui
 enable_gtk2_check
 enable_gnome_check
 enable_gtk4_check
+enable_gtk4_hwaccel
 enable_gtk3_check
 enable_motif_check
 enable_gtktest
@@ -1541,6 +1542,7 @@ Optional Features:
   --enable-gtk2-check     If auto-select GUI, check for GTK+ 2 default=yes
   --enable-gnome-check    If GTK GUI, check for GNOME default=no
   --enable-gtk4-check     If auto-select GUI, check for GTK 4 default=yes
+  --enable-gtk4-hwaccel  Use hardware accelerated rendering backend for GTK4 
default=no
   --enable-gtk3-check     If auto-select GUI, check for GTK+ 3 default=yes
   --enable-motif-check    If auto-select GUI, check for Motif default=yes
   --disable-gtktest       Do not try to compile and run a test GTK program
@@ -10604,6 +10606,28 @@ printf "%s
" "$enable_gtk4_check" >&6; }
   fi
 fi
 
+if test "x$SKIP_GTK4" != "xYES" -a "$enable_gui_canon" = "gtk4"; then
+  { printf "%s
" "$as_me:${as_lineno-$LINENO}: checking --enable-gtk4-hwaccel argument" >&5
+printf %s "checking --enable-gtk4-hwaccel argument... " >&6; }
+  # Check whether --enable-gtk4-hwaccel was given.
+if test ${enable_gtk4_hwaccel+y}
+then :
+  enableval=$enable_gtk4_hwaccel;
+else case e in #(
+  e) enable_gtk4_hwaccel="no" ;;
+esac
+fi
+
+  { printf "%s
" "$as_me:${as_lineno-$LINENO}: result: $enable_gtk4_hwaccel" >&5
+printf "%s
" "$enable_gtk4_hwaccel" >&6; }
+
+  if test "x$enable_gtk4_hwaccel" = "xyes"; then
+    printf "%s
" "#define USE_GTK4_SNAPSHOT 1" >>confdefs.h
+
+  fi
+fi
+
+
 if test "x$SKIP_GTK3" != "xYES" -a "$enable_gui_canon" != "gtk3"; then
   { printf "%s
" "$as_me:${as_lineno-$LINENO}: checking whether or not to look for GTK+ 3" >&5
 printf %s "checking whether or not to look for GTK+ 3... " >&6; }
diff --git a/src/config.h.in b/src/config.h.in
index e4273f5b7..786132989 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -499,6 +499,9 @@
 /* Define if GTK GUI is to be linked against GTK 4 */
 #undef USE_GTK4
 
+/* Define if GTK4 GUI should use hardware accelerated backend */
+#undef USE_GTK4_SNAPSHOT
+
 /* Define if we have isinf() */
 #undef HAVE_ISINF
 
diff --git a/src/configure.ac b/src/configure.ac
index cf6a59f6c..06f03344c 100644
--- a/src/configure.ac
+++ b/src/configure.ac
@@ -2715,6 +2715,19 @@ if test "x$SKIP_GTK4" != "xYES" -a "$enable_gui_canon" 
!= "gtk4"; then
   fi
 fi
 
+if test "x$SKIP_GTK4" != "xYES" -a "$enable_gui_canon" = "gtk4"; then
+  AC_MSG_CHECKING(--enable-gtk4-hwaccel argument)
+  AC_ARG_ENABLE(gtk4-hwaccel,
+       [  --enable-gtk4-hwaccel  Use hardware accelerated rendering backend 
for GTK4 [default=no]],
+       , enable_gtk4_hwaccel="no")
+  AC_MSG_RESULT($enable_gtk4_hwaccel)
+
+  if test "x$enable_gtk4_hwaccel" = "xyes"; then
+    AC_DEFINE(USE_GTK4_SNAPSHOT)
+  fi
+fi
+
+
 if test "x$SKIP_GTK3" != "xYES" -a "$enable_gui_canon" != "gtk3"; then
   AC_MSG_CHECKING(whether or not to look for GTK+ 3)
   AC_ARG_ENABLE(gtk3-check,
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 80e2a6339..45002c5bf 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -7272,6 +7272,13 @@ f_has(typval_T *argvars, typval_T *rettv)
                1
 #else
                0
+#endif
+               },
+       {"image_gdk",
+#ifdef FEAT_IMAGE_GDK
+               1
+#else
+               0
 #endif
                },
        {"image_kitty",
diff --git a/src/feature.h b/src/feature.h
index 59cd7bb48..3a0b6931c 100644
--- a/src/feature.h
+++ b/src/feature.h
@@ -1132,7 +1132,11 @@
 #endif
 
 #if defined(FEAT_IMAGE) && defined(FEAT_GUI_GTK)
-# define FEAT_IMAGE_CAIRO
+# ifdef USE_GTK4_SNAPSHOT
+#  define FEAT_IMAGE_GDK
+# else
+#  define FEAT_IMAGE_CAIRO
+# endif
 #endif
 
 /*
diff --git a/src/gui.c b/src/gui.c
index 680f90fd3..406d80425 100644
--- a/src/gui.c
+++ b/src/gui.c
@@ -1412,7 +1412,10 @@ gui_update_cursor(
            --gui.col;
 #endif
 
-#ifndef FEAT_GUI_MSWIN     // doesn't seem to work for MSWindows
+       // Doesn't seem to work for MSWindows. Not necessary when using
+       // GtkSnapshot, because everything is drawn in order in the snapshot
+       // vfunc.
+#if !defined(FEAT_GUI_MSWIN) && !defined(USE_GTK4_SNAPSHOT)
        gui.highlight_mask = ScreenAttrs[LineOffset[gui.row] + gui.col];
        (void)gui_screenchar(LineOffset[gui.row] + gui.col,
                GUI_MON_TRS_CURSOR | GUI_MON_NOCLEAR,
@@ -1605,6 +1608,10 @@ again:
     gui.num_cols = (pixel_width - gui_get_base_width()) / gui.char_width;
     gui.num_rows = (pixel_height - gui_get_base_height()) / gui.char_height;
 
+#ifdef USE_GTK4_SNAPSHOT
+    gui_gtk4_update_size();
+#endif
+
     gui_position_components(pixel_width);
     gui_reset_scroll_region();
 
diff --git a/src/gui.h b/src/gui.h
index 771e5d307..674601e0f 100644
--- a/src/gui.h
+++ b/src/gui.h
@@ -385,7 +385,9 @@ typedef struct Gui
     GdkColor   *spcolor;           // GDK-styled special color
 # endif
 # if defined(USE_GTK3) || defined(USE_GTK4)
+#  ifndef USE_GTK4_SNAPSHOT
     cairo_surface_t *surface;       // drawarea surface
+#  endif
 # else
     GdkGC      *text_gc;           // cached GC for normal text
 # endif
diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c
index dd4946793..77ac05636 100644
--- a/src/gui_gtk4.c
+++ b/src/gui_gtk4.c
@@ -30,6 +30,9 @@
 #include <gtk/gtk.h>
 #include "gui_gtk4_f.h"
 #include "gui_gtk4_cb.h"
+#ifdef USE_GTK4_SNAPSHOT
+# include "gui_gtk4_da.h"
+#endif
 
 /*
  * Geometry string parser, replacing XParseGeometry to remove X11 dependency.
@@ -265,7 +268,9 @@ modifiers_gdk2vim(guint state)
 static GtkWidget *vbox;                // the main vertical box
 
 // Forward declarations for event callbacks
+#ifndef USE_GTK4_SNAPSHOT
 static void draw_event(GtkDrawingArea *area, cairo_t *cr, int width, int 
height, gpointer data);
+#endif
 static gboolean key_press_event(GtkEventControllerKey *controller, guint 
keyval, guint keycode, GdkModifierType state, gpointer data);
 static void key_release_event(GtkEventControllerKey *controller, guint keyval, 
guint keycode, GdkModifierType state, gpointer data);
 static void button_press_event(GtkGestureClick *gesture, int n_press, double 
x, double y, gpointer data);
@@ -285,11 +290,15 @@ static void on_tab_reordered(GtkNotebook *notebook, 
gpointer *page, gint idx, gp
 static void mainwin_destroy_cb(GObject *object, gpointer data);
 static gboolean delete_event_cb(GtkWindow *window, gpointer data);
 static void mainwin_fullscreened_cb(GObject *obj, GParamSpec *pspec, gpointer 
user_data);
+#ifndef USE_GTK4_SNAPSHOT
 static void drawarea_realize_cb(GtkWidget *widget, gpointer data);
+#endif
 static void drawarea_unrealize_cb(GtkWidget *widget, gpointer data);
+#ifndef USE_GTK4_SNAPSHOT
 static void drawarea_resize_cb(GtkDrawingArea *area, int width, int height, 
gpointer data);
 static void drawarea_scale_factor_cb(GObject *object, GParamSpec *pspec, 
gpointer data);
 static cairo_surface_t *create_backing_surface(int width, int height);
+#endif
 static void clipboard_changed_cb(GdkClipboard *clipboard, gpointer user_data);
 #ifdef FEAT_MENU
 static void show_menubar_popover(void);
@@ -508,25 +517,31 @@ gui_mch_init(void)
     gtk_box_append(GTK_BOX(vbox), gui.formwin);
 
     // The drawing area for the editor content.
+#ifdef USE_GTK4_SNAPSHOT
+    gui.drawarea = vim_draw_area_new();
+#else
     gui.drawarea = gtk_drawing_area_new();
     gui.surface = NULL;
+#endif
     gtk_widget_set_focusable(gui.drawarea, TRUE);
     gtk_widget_set_vexpand(gui.drawarea, TRUE);
     gtk_widget_set_hexpand(gui.drawarea, TRUE);
     vim_form_put(VIM_FORM(gui.formwin), gui.drawarea, 0, 0);
 
+#ifndef USE_GTK4_SNAPSHOT
     // Set up drawing.
     gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(gui.drawarea),
            (GtkDrawingAreaDrawFunc)draw_event, NULL, NULL);
 
-    g_signal_connect(G_OBJECT(gui.drawarea), "realize",
-                    G_CALLBACK(drawarea_realize_cb), NULL);
-    g_signal_connect(G_OBJECT(gui.drawarea), "unrealize",
-                    G_CALLBACK(drawarea_unrealize_cb), NULL);
     g_signal_connect(G_OBJECT(gui.drawarea), "resize",
                     G_CALLBACK(drawarea_resize_cb), NULL);
     g_signal_connect(G_OBJECT(gui.drawarea), "notify::scale-factor",
                     G_CALLBACK(drawarea_scale_factor_cb), NULL);
+    g_signal_connect(G_OBJECT(gui.drawarea), "realize",
+                    G_CALLBACK(drawarea_realize_cb), NULL);
+#endif
+    g_signal_connect(G_OBJECT(gui.drawarea), "unrealize",
+                    G_CALLBACK(drawarea_unrealize_cb), NULL);
 
     // Set up event controllers.
     {
@@ -614,6 +629,7 @@ gui_mch_init(void)
     return OK;
 }
 
+#ifndef USE_GTK4_SNAPSHOT
 /*
  * Called when the foreground or background color has been changed.
  */
@@ -630,11 +646,14 @@ surface_fill_bg(void)
        cairo_destroy(cr);
     }
 }
+#endif
 
     void
 gui_mch_new_colors(void)
 {
+#ifndef USE_GTK4_SNAPSHOT
     surface_fill_bg();
+#endif
     if (gui.drawarea != NULL && gtk_widget_get_realized(gui.drawarea))
        gtk_widget_queue_draw(gui.drawarea);
 }
@@ -1336,6 +1355,23 @@ gui_mch_get_rgb(guicolor_T pixel)
  * ============================================================
  */
 
+#ifdef USE_GTK4_SNAPSHOT
+    void
+gui_gtk4_update_size(void)
+{
+    vim_draw_area_set_size(VIM_DRAW_AREA(gui.drawarea),
+           gui.num_rows, gui.num_cols);
+}
+
+# ifdef FEAT_NETBEANS_INTG
+    cairo_t *
+gui_gtk4_get_multisign_context(int x, int y, int w, int h)
+{
+    return vim_draw_area_get_multisign_cairo(
+           VIM_DRAW_AREA(gui.drawarea), x, y, w, h);
+}
+# endif
+#else // USE_GTK4_SNAPSHOT
 static void set_cairo_source_from_pixel(cairo_t *cr, guicolor_T pixel);
 
     static void
@@ -1394,10 +1430,15 @@ create_backing_surface(int width, int height)
     cairo_surface_set_device_scale(surf, (double)scale, (double)scale);
     return surf;
 }
+#endif // !USE_GTK4_SNAPSHOT
 
     void
 gui_mch_clear_block(int row1, int col1, int row2, int col2)
 {
+#ifdef USE_GTK4_SNAPSHOT
+    vim_draw_area_clear_block(VIM_DRAW_AREA(gui.drawarea), row1,
+           col1, row2, col2);
+#else
     cairo_t *cr;
 
     if (gui.surface == NULL)
@@ -1411,6 +1452,7 @@ gui_mch_clear_block(int row1, int col1, int row2, int 
col2)
            (row2 - row1 + 1) * gui.char_height);
     cairo_fill(cr);
     cairo_destroy(cr);
+#endif
 
     if (gui.drawarea != NULL)
        gtk_widget_queue_draw(gui.drawarea);
@@ -1419,6 +1461,9 @@ gui_mch_clear_block(int row1, int col1, int row2, int 
col2)
     void
 gui_mch_clear_all(void)
 {
+#ifdef USE_GTK4_SNAPSHOT
+    vim_draw_area_clear(VIM_DRAW_AREA(gui.drawarea));
+#else
     cairo_t *cr;
 
     if (gui.surface == NULL)
@@ -1428,11 +1473,101 @@ gui_mch_clear_all(void)
     set_cairo_source_from_pixel(cr, gui.back_pixel);
     cairo_paint(cr);
     cairo_destroy(cr);
+#endif
 
     if (gui.drawarea != NULL)
        gtk_widget_queue_draw(gui.drawarea);
 }
 
+#ifdef FEAT_IMAGE_GDK
+    void
+gui_gtk4_remove_image(win_T *wp)
+{
+    vim_draw_area_remove_image(VIM_DRAW_AREA(gui.drawarea), wp->w_id);
+}
+
+    void
+gui_mch_free_popup_image(win_T *wp)
+{
+    if (wp->w_popup_image_texture != NULL)
+       g_clear_object(&wp->w_popup_image_texture);
+}
+
+/*
+ * If "wp->w_popup_image_texture" is NULL or "force" is TRUE, then create the
+ * cached GdkTexture object.
+ */
+    static void
+maybe_set_image_texture(win_T *wp, gboolean force)
+{
+    GdkMemoryFormat fmt;
+    size_t         stride;
+    GdkTexture     *texture;
+    GBytes         *bytes;
+    size_t         size;
+
+    if (!force && wp->w_popup_image_texture != NULL)
+       return;
+
+    if (wp->w_popup_image_alpha)
+    {
+       fmt = GDK_MEMORY_A8R8G8B8;
+       size = wp->w_popup_image_w * wp->w_popup_image_h * 4;
+       stride = wp->w_popup_image_w * 4;
+    }
+    else
+    {
+       fmt = GDK_MEMORY_R8G8B8;
+       size = wp->w_popup_image_w * wp->w_popup_image_h * 3;
+       stride = wp->w_popup_image_w * 3;
+    }
+
+    bytes = g_bytes_new(wp->w_popup_image_data, size);
+    texture = gdk_memory_texture_new(wp->w_popup_image_w,
+           wp->w_popup_image_h, fmt, bytes, stride);
+    g_bytes_unref(bytes);
+
+    if (wp->w_popup_image_texture != NULL)
+       g_object_unref(wp->w_popup_image_texture);
+    wp->w_popup_image_texture = texture;
+}
+
+    bool
+gui_mch_update_popup_image_pixels(win_T *wp)
+{
+    if (wp->w_popup_image_texture == NULL || wp->w_popup_image_data == NULL)
+       return false;
+    maybe_set_image_texture(wp, TRUE);
+    return true;
+}
+
+    void
+gui_mch_draw_popup_image(
+       win_T   *wp,
+       int      row,
+       int      col,
+       int      src_x,
+       int      src_y,
+       int      draw_w,
+       int      draw_h)
+{
+    if (wp->w_popup_image_data == NULL
+           || wp->w_popup_image_w <= 0 || wp->w_popup_image_h <= 0
+           || draw_w <= 0 || draw_h <= 0)
+       return;
+
+    maybe_set_image_texture(wp, FALSE);
+    if (gui.drawarea != NULL)
+    {
+       vim_draw_area_add_image(VIM_DRAW_AREA(gui.drawarea),
+               wp->w_popup_image_texture, row, col, src_x, src_y,
+               draw_w, draw_h, wp->w_zindex, wp->w_id);
+
+       gtk_widget_queue_draw(gui.drawarea);
+    }
+}
+#endif
+
 #ifdef FEAT_IMAGE_CAIRO
     void
 gui_mch_free_popup_image(win_T *wp)
@@ -1461,7 +1596,8 @@ gui_mch_draw_popup_image(
     if (wp->w_popup_image_data == NULL
            || wp->w_popup_image_w <= 0 || wp->w_popup_image_h <= 0
            || draw_w <= 0 || draw_h <= 0
-           || gui.surface == NULL)
+           || gui.surface == NULL
+           )
        return;
 
     x = FILL_X(col);
@@ -1474,6 +1610,7 @@ gui_mch_draw_popup_image(
 }
 #endif // FEAT_IMAGE_CAIRO
 
+#ifndef USE_GTK4_SNAPSHOT
     static void
 surface_copy_rect(int dest_x, int dest_y,
        int src_x, int src_y,
@@ -1500,10 +1637,16 @@ surface_copy_rect(int dest_x, int dest_y,
     cairo_destroy(cr);
     cairo_surface_destroy(tmp);
 }
+#endif
 
     void
 gui_mch_delete_lines(int row, int num_lines)
 {
+#ifdef USE_GTK4_SNAPSHOT
+    vim_draw_area_move_block(VIM_DRAW_AREA(gui.drawarea),
+           row, row + num_lines, gui.scroll_region_bot,
+           gui.scroll_region_left, gui.scroll_region_right);
+#else
     int ncols = gui.scroll_region_right - gui.scroll_region_left + 1;
     int nrows = gui.scroll_region_bot - row + 1;
     int src_nrows = nrows - num_lines;
@@ -1512,6 +1655,7 @@ gui_mch_delete_lines(int row, int num_lines)
            FILL_X(gui.scroll_region_left), FILL_Y(row),
            FILL_X(gui.scroll_region_left), FILL_Y(row + num_lines),
            gui.char_width * ncols + 1, gui.char_height * src_nrows);
+#endif
     gui_clear_block(
            gui.scroll_region_bot - num_lines + 1, gui.scroll_region_left,
            gui.scroll_region_bot, gui.scroll_region_right);
@@ -1522,6 +1666,11 @@ gui_mch_delete_lines(int row, int num_lines)
     void
 gui_mch_insert_lines(int row, int num_lines)
 {
+#ifdef USE_GTK4_SNAPSHOT
+    vim_draw_area_move_block(VIM_DRAW_AREA(gui.drawarea),
+           row + num_lines, row, gui.scroll_region_bot - num_lines,
+           gui.scroll_region_left, gui.scroll_region_right);
+#else
     int ncols = gui.scroll_region_right - gui.scroll_region_left + 1;
     int nrows = gui.scroll_region_bot - row + 1;
     int src_nrows = nrows - num_lines;
@@ -1530,6 +1679,7 @@ gui_mch_insert_lines(int row, int num_lines)
            FILL_X(gui.scroll_region_left), FILL_Y(row + num_lines),
            FILL_X(gui.scroll_region_left), FILL_Y(row),
            gui.char_width * ncols + 1, gui.char_height * src_nrows);
+#endif
     gui_clear_block(
            row, gui.scroll_region_left,
            row + num_lines - 1, gui.scroll_region_right);
@@ -1540,6 +1690,10 @@ gui_mch_insert_lines(int row, int num_lines)
     void
 gui_mch_draw_hollow_cursor(guicolor_T color)
 {
+#ifdef USE_GTK4_SNAPSHOT
+    gui_mch_set_fg_color(color);
+    vim_draw_area_set_hollow_cursor(VIM_DRAW_AREA(gui.drawarea));
+#else
     cairo_t *cr;
     int i = 1;
 
@@ -1559,6 +1713,7 @@ gui_mch_draw_hollow_cursor(guicolor_T color)
            i * gui.char_width - 1, gui.char_height - 1);
     cairo_stroke(cr);
     cairo_destroy(cr);
+#endif
 
     gtk_widget_queue_draw(gui.drawarea);
 }
@@ -1566,6 +1721,10 @@ gui_mch_draw_hollow_cursor(guicolor_T color)
     void
 gui_mch_draw_part_cursor(int w, int h, guicolor_T color)
 {
+#ifdef USE_GTK4_SNAPSHOT
+    gui_mch_set_fg_color(color);
+    vim_draw_area_set_part_cursor(VIM_DRAW_AREA(gui.drawarea), w, h);
+#else
     cairo_t *cr;
 
     if (gui.surface == NULL)
@@ -1577,13 +1736,14 @@ gui_mch_draw_part_cursor(int w, int h, guicolor_T color)
            gui.fgcolor->red, gui.fgcolor->green,
            gui.fgcolor->blue, gui.fgcolor->alpha);
     cairo_rectangle(cr,
-#ifdef FEAT_RIGHTLEFT
+# ifdef FEAT_RIGHTLEFT
            CURSOR_BAR_RIGHT ? FILL_X(gui.col + 1) - w :
-#endif
+# endif
            FILL_X(gui.col), FILL_Y(gui.row) + gui.char_height - h,
            w, h);
     cairo_fill(cr);
     cairo_destroy(cr);
+#endif
 
     gtk_widget_queue_draw(gui.drawarea);
 }
@@ -1592,8 +1752,10 @@ gui_mch_draw_part_cursor(int w, int h, guicolor_T color)
 gui_mch_flash(int msec)
 {
     // Invert the screen, wait, then invert back
+#ifndef USE_GTK4_SNAPSHOT
     if (gui.surface == NULL)
        return;
+#endif
 
     gui_mch_invert_rectangle(0, 0, (int)Rows - 1, (int)Columns - 1);
     gui_mch_flush();
@@ -1604,6 +1766,9 @@ gui_mch_flash(int msec)
     void
 gui_mch_invert_rectangle(int r, int c, int nr, int nc)
 {
+#ifdef USE_GTK4_SNAPSHOT
+    vim_draw_area_invert_block(VIM_DRAW_AREA(gui.drawarea), r, c, nr, nc);
+#else
     cairo_t *cr;
 
     if (gui.surface == NULL)
@@ -1617,6 +1782,7 @@ gui_mch_invert_rectangle(int r, int c, int nr, int nc)
            (nc + 1) * gui.char_width, (nr + 1) * gui.char_height);
     cairo_fill(cr);
     cairo_destroy(cr);
+#endif
 
     gtk_widget_queue_draw(gui.drawarea);
 }
@@ -1961,6 +2127,7 @@ focus_out_event(GtkEventControllerFocus *controller 
UNUSED,
     }
 }
 
+#ifndef USE_GTK4_SNAPSHOT
     static void
 drawarea_realize_cb(GtkWidget *widget UNUSED, gpointer data UNUSED)
 {
@@ -1987,6 +2154,7 @@ drawarea_realize_cb(GtkWidget *widget UNUSED, gpointer 
data UNUSED)
 
     gui_mch_new_colors();
 }
+#endif
 
     static void
 drawarea_unrealize_cb(GtkWidget *widget UNUSED, gpointer data UNUSED)
@@ -1994,13 +2162,16 @@ drawarea_unrealize_cb(GtkWidget *widget UNUSED, 
gpointer data UNUSED)
 #ifdef FEAT_XIM
     im_shutdown();
 #endif
+#ifndef USE_GTK4_SNAPSHOT
     if (gui.surface != NULL)
     {
        cairo_surface_destroy(gui.surface);
        gui.surface = NULL;
     }
+#endif
 }
 
+#ifndef USE_GTK4_SNAPSHOT
 // Debounced resize: drawarea_resize_cb only resizes the backing surface
 // (preserving old content) and (re)arms a short timeout. The actual
 // gui_resize_shell() runs from drawarea_resize_apply_cb once the user has
@@ -2120,6 +2291,7 @@ drawarea_scale_factor_cb(GObject *object UNUSED,
     if (gui.in_use)
        redraw_all_later(UPD_CLEAR);
 }
+#endif
 
 #ifdef FEAT_DND
 /*
@@ -2683,6 +2855,16 @@ on_tab_reordered(
     void
 gui_mch_drawsign(int row, int col, int typenr)
 {
+# ifdef USE_GTK4_SNAPSHOT
+    GdkTexture *sign;
+
+    sign = (GdkTexture *)sign_get_image(typenr);
+    if (sign == NULL)
+       return;
+
+    vim_draw_area_add_sign(VIM_DRAW_AREA(gui.drawarea), sign,
+           row, col, SIGN_WIDTH, SIGN_HEIGHT);
+# else
     GdkPixbuf  *sign;
     cairo_t    *cr;
     int                width, height;
@@ -2717,6 +2899,7 @@ gui_mch_drawsign(int row, int col, int typenr)
 
     cairo_paint(cr);
     cairo_destroy(cr);
+# endif
 
     gtk_widget_queue_draw(gui.drawarea);
 }
@@ -2726,12 +2909,21 @@ gui_mch_register_sign(char_u *signfile)
 {
     if (signfile[0] != NUL && signfile[0] != '-' && gui.in_use)
     {
-       GdkPixbuf   *sign;
-       GError      *error = NULL;
+       GError *error = NULL;
+# ifdef USE_GTK4_SNAPSHOT
+       GdkTexture  *sign;
+
+       sign = gdk_texture_new_from_filename((const char *)signfile,
+                                                           &error);
+       if (sign != NULL)
+           return sign;
+# else
+       GdkPixbuf *sign;
 
        sign = gdk_pixbuf_new_from_file((const char *)signfile, &error);
        if (error == NULL)
            return sign;
+# endif
 
        semsg("E255: %s", error->message);
        g_error_free(error);
@@ -2887,6 +3079,7 @@ setup_zero_width_cluster(PangoItem *item, PangoGlyphInfo 
*glyph,
        glyph->geometry.x_offset = -width + MAX(0, width - ink_rect.width) / 2;
 }
 
+#ifndef USE_GTK4_SNAPSHOT
 /*
  * Draw a single glyph string segment: background, foreground, and fake bold.
  */
@@ -2972,6 +3165,7 @@ draw_under(int flags, int row, int col, int cells, 
cairo_t *cr)
        cairo_stroke(cr);
     }
 }
+#endif
 
 /*
  * Draw a string of characters on the screen.
@@ -2988,15 +3182,24 @@ gui_gtk_draw_string_ext(
        int     flags,
        int     force_pango)
 {
-    GdkRectangle       area;
     PangoGlyphString   *glyphs;
     int                        column_offset = 0;
     int                        i;
+#ifdef USE_GTK4_SNAPSHOT
+    gboolean           s_alloced = FALSE;
+#else
+    GdkRectangle       area;
     cairo_t            *cr;
+#endif
 
-    if (gui.text_context == NULL || gui.surface == NULL)
+    if (gui.text_context == NULL
+#ifndef USE_GTK4_SNAPSHOT
+           || gui.surface == NULL
+#endif
+           )
        return len;
 
+#ifndef USE_GTK4_SNAPSHOT
     // Restrict all drawing to the current screen line.
     area.x = gui.border_offset;
     area.y = FILL_Y(row);
@@ -3006,6 +3209,7 @@ gui_gtk_draw_string_ext(
     cr = cairo_create(gui.surface);
     cairo_rectangle(cr, area.x, area.y, area.width, area.height);
     cairo_clip(cr);
+#endif
 
     glyphs = pango_glyph_string_new();
 
@@ -3030,7 +3234,12 @@ gui_gtk_draw_string_ext(
            glyphs->log_clusters[i] = i;
        }
 
+#ifdef USE_GTK4_SNAPSHOT
+       vim_draw_area_add_glyphs(VIM_DRAW_AREA(gui.drawarea), row, col, len,
+               flags, gui.ascii_font, glyphs);
+#else
        draw_glyph_string(row, col, len, flags, gui.ascii_font, glyphs, cr);
+#endif
 
        column_offset = len;
     }
@@ -3046,8 +3255,17 @@ not_ascii:;
        // Safety check: pango crashes with invalid utf-8.
        if (!utf_valid_string(s, s + len))
        {
+#ifdef USE_GTK4_SNAPSHOT
+           // vim_draw_area_add_glyphs() also handles under decorations. Make
+           // "str" a string of spaces so that under decorations are still
+           // applied.
+           s = g_malloc(len);
+           memset(s, ' ', len);
+           s_alloced = TRUE;
+#else
            column_offset = len;
            goto skipitall;
+#endif
        }
 
        cluster_width = PANGO_SCALE * gui.char_width;
@@ -3139,8 +3357,14 @@ not_ascii:;
                }
            }
 
+#ifdef USE_GTK4_SNAPSHOT
+           vim_draw_area_add_glyphs(VIM_DRAW_AREA(gui.drawarea),
+                   row, col + column_offset, item_cells,
+                   flags, item->analysis.font, glyphs);
+#else
            draw_glyph_string(row, col + column_offset, item_cells,
                              flags, item->analysis.font, glyphs, cr);
+#endif
 
            pango_item_free(item);
 
@@ -3150,12 +3374,17 @@ not_ascii:;
        pango_attr_list_unref(attr_list);
     }
 
+#ifdef USE_GTK4_SNAPSHOT
+    if (s_alloced)
+       g_free(s);
+#else
 skipitall:
     draw_under(flags, row, col, column_offset, cr);
+    cairo_destroy(cr);
+#endif
 
     pango_glyph_string_free(glyphs);
 
-    cairo_destroy(cr);
 
     if (gui.drawarea != NULL)
        gtk_widget_queue_draw(gui.drawarea);
@@ -3185,7 +3414,11 @@ gui_gtk_draw_string(int row, int col, char_u *s, int 
len, int flags)
     int                is_utf8;
     char_u     backup_ch;
 
-    if (gui.text_context == NULL || gui.surface == NULL)
+    if (gui.text_context == NULL
+#ifndef USE_GTK4_SNAPSHOT
+           || gui.surface == NULL
+#endif
+           )
        return len;
 
     if (output_conv.vc_type != CONV_NONE)
@@ -3907,16 +4140,15 @@ create_toolbar_icon(vimmenu_T *menu)
        expand_env(menu->iconfile, buf, MAXPATHL);
        if (vim_fexists(buf))
        {
-           GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file_at_scale(
-                   (const char *)buf, 24, 24, TRUE, NULL);
-           if (pixbuf != NULL)
+           GdkTexture *texture = gdk_texture_new_from_filename(
+                   (const char *)buf, NULL);
+
+           if (texture != NULL)
            {
-               GdkTexture *texture =
-                       gdk_texture_new_for_pixbuf(pixbuf);
                image = gtk_image_new_from_paintable(
                        GDK_PAINTABLE(texture));
+               gtk_widget_set_size_request(image, 24, 24);
                g_object_unref(texture);
-               g_object_unref(pixbuf);
            }
        }
     }
@@ -5220,7 +5452,6 @@ print_draw_page_cb(
     linenr_T       lnum;
     linenr_T       first;
     linenr_T       last;
-    int                    page_line;
     double         y;
 
     cr = gtk_print_context_get_cairo_context(context);
@@ -5231,9 +5462,8 @@ print_draw_page_cb(
        last = pd->last_line;
 
     y = 0;
-    page_line = 0;
 
-    for (lnum = first; lnum <= last; ++lnum, ++page_line)
+    for (lnum = first; lnum <= last; ++lnum)
     {
        char_u          *line;
        PangoLayout     *layout;
diff --git a/src/gui_gtk4_da.c b/src/gui_gtk4_da.c
new file mode 100644
index 000000000..34dad93a6
--- /dev/null
+++ b/src/gui_gtk4_da.c
@@ -0,0 +1,1600 @@
+/* vi:set ts=8 sts=4 sw=4 noet:
+ *
+ * VIM - Vi IMproved           by Bram Moolenaar
+ *
+ * Do ":help uganda"  in Vim to read copying and usage conditions.
+ * Do ":help credits" in Vim to see a list of people who contributed.
+ * See README.txt for an overview of the Vim source code.
+ */
+
+#include "vim.h"
+
+#ifdef USE_GTK4_SNAPSHOT
+
+#include <gtk/gtk.h>
+#include "gui_gtk4_da.h"
+
+#define DRAW_NODE_DIRTY 1   // Draw node is dirty
+#define DRAW_NODE_NOBG 2    // Don't create background node
+#define DRAW_NODE_NOINK 4   // Draw node has no ink
+#define DRAW_NODE_UNDER 8   // Has under decorations (for convenience)
+#define DRAW_NODE_CLIP 16   // Text node should be clipped to draw node bounds.
+
+typedef struct
+{
+    int refcount;
+
+    PangoGlyphInfo  *glyphs;
+    int                    n_glyphs;
+    char_u         dnode_flags; // DRAW_NODE_* flags
+    GskRenderNode   *node;  // This is either a text node, or a container node
+                           // (if there is more than one node).
+
+    PangoFont  *font;
+    GdkRGBA    fg_color;
+    GdkRGBA    bg_color;
+    GdkRGBA    sp_color;
+    int                flags;      // DRAW_* flags
+
+    int start_col;
+    int n_cells;
+} DrawNode;
+
+#define END_COL(dn) ((dn)->start_col + (dn)->n_cells - 1)
+#define HAS_INK(r) ((r)->width != 0 || (r)->height != 0)
+
+/*
+ * Each cell holds its own reference to a draw node if any. A draw node may 
span
+ * multiple cells, which represents how many cells it takes up on screen.
+ */
+typedef struct
+{
+    DrawNode   *dnode; // May be NULL
+    gboolean   invert; // If this cell is inverted
+} DrawCell;
+
+#ifdef FEAT_IMAGE_GDK
+/*
+ * Struct containing information about an image. This is designed to map well
+ * with how Vim handles the kitty graphics protocol.
+ */
+typedef struct
+{
+    int id;
+    int zindex;
+    GskRenderNode *node; // Cached clip node, which has the texture node as its
+                        // child. May be NULL
+} DrawImage;
+#endif
+
+struct _VimDrawArea
+{
+    GtkWidget parent;
+
+    DrawCell   *cells; // May be NULL, always check!
+    int         n_rows;
+    int                n_cols;
+
+    int                resize_count;
+
+    // Used for hollow and part style cursors. For the block cursor, that is
+    // simply rendered as a cell using vim_draw_area_add_glyphs(). May be NULL.
+    GskRenderNode *cursor_node;
+
+#if defined(FEAT_SIGN_ICONS)
+    // Queue of sign icon render nodes. Icons at the end of the queue are drawn
+    // ontop of earlier ones.
+    GQueue *signs;
+#endif
+
+#ifdef FEAT_NETBEANS_INTG
+    // Cairo render node for multi sign indicator for Netbeans. May be NULL
+    GskRenderNode *multisign_node;
+#endif
+
+#ifdef FEAT_IMAGE_GDK
+    // Queue of DrawImage structs. Sorted in ascending order of zindex, so that
+    // images with a higher zindex are rendered over ones with lower zindex.
+    GQueue *images;
+#endif
+};
+
+#define GET_ROW(da, n) ((da)->cells + (da)->n_cols * (n))
+
+G_DEFINE_TYPE(VimDrawArea, vim_draw_area, GTK_TYPE_WIDGET)
+
+#ifdef FEAT_IMAGE_GDK
+static void draw_image_free(DrawImage *dimg);
+#endif
+static void vim_draw_area_snapshot(GtkWidget *widget, GtkSnapshot *snapshot);
+static void vim_draw_area_size_allocate(GtkWidget *widget, int width, int 
height, int baseline);
+
+    static void
+vim_draw_area_finalize(GObject *obj)
+{
+    VimDrawArea *self = VIM_DRAW_AREA(obj);
+
+    // "multisign_node" and "cursor_node" will be freed in
+    // vim_draw_area_clear_block().
+    vim_draw_area_clear(self);
+
+    g_free(self->cells);
+#ifdef FEAT_SIGN_ICONS
+    // vim_draw_area_clear_block() should have removed all the sign icons
+    assert(g_queue_is_empty(self->signs));
+    g_queue_free(self->signs);
+#endif
+#ifdef FEAT_IMAGE_GDK
+    g_queue_free_full(self->images, (GDestroyNotify)draw_image_free);
+#endif
+
+    G_OBJECT_CLASS(vim_draw_area_parent_class)->finalize(obj);
+}
+
+    static void
+vim_draw_area_class_init(VimDrawAreaClass *class)
+{
+    GtkWidgetClass  *widget_class = GTK_WIDGET_CLASS(class);
+    GObjectClass    *obj_class = G_OBJECT_CLASS(class);
+
+    widget_class->snapshot = vim_draw_area_snapshot;
+    widget_class->size_allocate = vim_draw_area_size_allocate;
+
+    obj_class->finalize = vim_draw_area_finalize;
+
+}
+
+    static void
+vim_draw_area_init(VimDrawArea *self)
+{
+#ifdef FEAT_SIGN_ICONS
+    self->signs = g_queue_new();
+#endif
+#ifdef FEAT_IMAGE_GDK
+    self->images = g_queue_new();
+#endif
+}
+
+    GtkWidget *
+vim_draw_area_new(void)
+{
+    return g_object_new(VIM_TYPE_DRAW_AREA, NULL);
+}
+
+/*
+ * Set the size of the draw area to "rows" and "cols".
+ */
+    void
+vim_draw_area_set_size(VimDrawArea *self, int rows, int cols)
+{
+    if (self->cells != NULL && self->n_rows == rows && self->n_cols == cols)
+       return;
+    if (rows == 0 || cols == 0)
+       return;
+
+    vim_draw_area_clear(self);
+
+    self->n_rows = rows;
+    self->n_cols = cols;
+    self->cells = g_realloc_n(self->cells, rows * cols, sizeof(DrawCell));
+    memset(self->cells, 0, rows * (sizeof(DrawCell) * cols));
+    self->resize_count++;
+}
+
+    static void
+node_unref(GskRenderNode *node)
+{
+    if (node != NULL)
+       gsk_render_node_unref(node);
+}
+
+/*
+ * Return TRUE if "glyphs" take up space (not entirely whitespace).
+ */
+    static gboolean
+glyphs_has_ink(PangoFont *font, const PangoGlyphInfo *glyphs, int n_glyphs)
+{
+    for (int i = 0; i < n_glyphs; i++)
+    {
+       PangoRectangle glyph_ink;
+
+       pango_font_get_glyph_extents (font, glyphs[i].glyph, &glyph_ink, NULL);
+
+       if (HAS_INK(&glyph_ink))
+           return TRUE;
+    }
+    return FALSE;
+}
+
+/*
+ * Realloc "glyphs" to "n_glyphs" and return the new reallocated pointer.
+ */
+    static PangoGlyphInfo *
+glyphs_resize(PangoGlyphInfo *glyphs, int n_glyphs)
+{
+    return g_realloc_n(glyphs, n_glyphs, sizeof(PangoGlyphInfo));
+}
+
+/*
+ * Return TRUE if "bg" is the same as the default background color.
+ */
+    static gboolean
+color_is_default_bg(const GdkRGBA *bg)
+{
+    guicolor_T bgc = ((guicolor_T)(bg->red * 255) << 16)
+       | ((guicolor_T)(bg->green * 255) << 8)
+       |  (guicolor_T)(bg->blue * 255);
+    return bgc == gui.back_pixel;
+}
+
+/*
+ * Convert the given cell offset into an index in the "glyphs" array.
+ */
+    static int
+cell_offset_to_glyph(const PangoGlyphInfo *glyphs, int n_glyphs, int 
cell_offset)
+{
+    int cells_seen = 0;
+
+    for (int i = 0; i < n_glyphs; i++)
+    {
+       const PangoGlyphInfo *glyph = glyphs + i;
+
+       if (cells_seen >= cell_offset)
+           return i;
+
+       cells_seen += glyph->geometry.width / (gui.char_width * PANGO_SCALE);
+    }
+    return n_glyphs;
+}
+
+/*
+ * Create a new under decoration node with the given flags. Returns NULL if no
+ * under decorations are needed.
+ */
+    static GskRenderNode *
+create_under_decor_node(
+       int             row,
+       int             start_col,
+       int             n_cells,
+       int             flags,
+       const GdkRGBA   *fg_color,
+       const GdkRGBA   *sp_color)
+{
+    GskRenderNode   *nodes[3];
+    int                    n_nodes = 0;
+    GskRenderNode   *container;
+
+    if (flags & DRAW_UNDERL)
+       nodes[n_nodes++] = gsk_color_node_new(fg_color,
+               &GRAPHENE_RECT_INIT(FILL_X(start_col),
+                   FILL_Y(row + 1) - 1,
+                   FILL_X(start_col + n_cells) - FILL_X(start_col), 1));
+
+    if (flags & DRAW_STRIKE)
+       nodes[n_nodes++] = gsk_color_node_new(fg_color,
+               &GRAPHENE_RECT_INIT(FILL_X(start_col),
+                   FILL_Y(row) + (int)(gui.char_height / 2),
+                   FILL_X(start_col + n_cells) - FILL_X(start_col), 1));
+
+    if (flags & DRAW_UNDERC)
+    {
+       int             y = FILL_Y(row + 1) - 1; // Top of underneath line,
+                                                // upwards by one pixel.
+       int             x_start = FILL_X(start_col);
+       int             x_end = FILL_X(start_col + n_cells);
+
+       // GskPath was added in GSK 4.14, otherwise use cairo
+#if GTK_CHECK_VERSION(4, 14, 0)
+       GskPathBuilder  *builder;
+       GskPath         *path;
+       GskStroke       *stroke;
+       GskRenderNode   *color_node;
+       graphene_rect_t bounds;
+
+       const int       half_wave = 4;  // Half-cycle width (e.g., 4px up, 4px
+                                       // down)
+       const int       amplitude = 2;  // Peak height from baseline
+       int             toggle = -1;    // Start by pulling up (-Y is up in GTK)
+
+       builder = gsk_path_builder_new();
+       gsk_path_builder_move_to(builder, x_start, y);
+
+       // Each cycle contains two quadratic bezier curves, one going up and one
+       // going down.
+       for (int x = x_start; x < x_end; x += half_wave)
+       {
+           int current_half = half_wave;
+           if (x + current_half > x_end)
+           {
+               current_half = x_end - x;
+           }
+
+           // The control point sits exactly halfway horizontally through the 
arc
+           int cp_x = x + (current_half / 2);
+           int cp_y = y + (toggle * amplitude);
+           int end_x = x + current_half;
+
+           gsk_path_builder_quad_to(builder, cp_x, cp_y, end_x, y);
+
+           toggle = -toggle; // Flip direction for the next half-wave
+       }
+
+       path = gsk_path_builder_free_to_path(builder);
+       stroke = gsk_stroke_new(1.0f);
+
+       gsk_path_get_stroke_bounds (path, stroke, &bounds);
+       color_node = gsk_color_node_new(sp_color, &bounds);
+
+       nodes[n_nodes++] = gsk_stroke_node_new(color_node, path, stroke);
+       gsk_stroke_free(stroke);
+       gsk_path_unref(path);
+       gsk_render_node_unref(color_node);
+#else
+       static const int    val[8] = {1, 0, 0, 0, 1, 2, 2, 2};
+       cairo_t             *cr;
+       GskRenderNode       *node;
+
+       node = gsk_cairo_node_new(
+               &GRAPHENE_RECT_INIT(x_start, y - 3, x_end - x_start, 5));
+       cr = gsk_cairo_node_get_draw_context(node);
+
+       cairo_set_line_width(cr, 1.0);
+       cairo_set_source_rgba(cr, sp_color->red, sp_color->green,
+               sp_color->blue, sp_color->alpha);
+
+       cairo_move_to(cr, x_start + 1, y - 2 + 0.5);
+
+       for (int i = x_start + 1; i < x_end; ++i)
+       {
+           int offset = val[i % 8];
+           cairo_line_to(cr, i, y - offset + 0.5);
+       }
+
+       cairo_stroke(cr);
+       cairo_destroy(cr);
+       nodes[n_nodes++] = node;
+#endif
+    }
+
+    if (n_nodes == 0)
+       return NULL;
+    if (n_nodes == 1)
+       return nodes[0];
+
+    container = gsk_container_node_new(nodes, n_nodes);
+    for (int i = 0; i < n_nodes; i++)
+       // Container node takes its own reference to each.
+       gsk_render_node_unref(nodes[i]);
+    return container;
+}
+
+/*
+ * Create a new draw node with a reference count of 1. Note that this may be
+ * NULL if creating a new draw node is not necessary.
+ */
+    static DrawNode *
+draw_node_new(
+       PangoFont               *font,
+       const PangoGlyphInfo    *glyphs,
+       int                     n_glyphs,
+       const GdkRGBA           *bg_color,
+       const GdkRGBA           *fg_color,
+       const GdkRGBA           *sp_color,
+       int                     flags,
+       int                     start_col,
+       int                     n_cells)
+{
+    DrawNode   *dnode;
+    gboolean   has_ink = glyphs_has_ink(font, glyphs, n_glyphs);
+    gboolean   is_def_bg = color_is_default_bg(bg_color);
+    gboolean   has_under = flags & (DRAW_UNDERL | DRAW_UNDERC | DRAW_STRIKE);
+
+    // If there is no ink to be displayed, and the background color is the same
+    // as the default background color (the color that will be displayed behind
+    // everything), then there is no point in creating a new draw node.
+    if (!has_ink && !has_under && (flags & DRAW_TRANSP || is_def_bg))
+       return NULL;
+
+    dnode = g_new0(DrawNode, 1);
+
+    dnode->refcount = 1;
+
+    dnode->glyphs = g_memdup2(glyphs, sizeof(PangoGlyphInfo) * n_glyphs);
+    dnode->n_glyphs = n_glyphs;
+    dnode->dnode_flags |= DRAW_NODE_DIRTY;
+    if (is_def_bg || flags & DRAW_TRANSP)
+       dnode->dnode_flags |= DRAW_NODE_NOBG;
+    if (!has_ink)
+       dnode->dnode_flags |= DRAW_NODE_NOINK;
+    if (has_under)
+       dnode->dnode_flags |= DRAW_NODE_UNDER;
+
+    dnode->font = g_object_ref(font);
+    dnode->bg_color = *bg_color;
+    dnode->fg_color = *fg_color;
+    dnode->sp_color = *sp_color;
+    dnode->flags = flags;
+
+    dnode->start_col = start_col;
+    dnode->n_cells = n_cells;
+
+    return dnode;
+}
+
+    static DrawNode *
+draw_node_ref(DrawNode *dnode)
+{
+    dnode->refcount++;
+    return dnode;
+}
+
+    static void
+draw_node_unref(DrawNode *dnode)
+{
+    if (dnode != NULL && --dnode->refcount <= 0)
+    {
+       g_free(dnode->glyphs);
+       node_unref(dnode->node);
+       g_object_unref(dnode->font);
+       g_free(dnode);
+    }
+}
+
+/*
+ * Dirty the draw node. This will remove the render node if any, and mark it to
+ * have a new render node created for it on the next snapshot vfunc call.
+ * Returns TRUE if draw node is not necessary anymore.
+ */
+    static gboolean
+draw_node_make_dirty(DrawNode *dnode)
+{
+    int flags = dnode->dnode_flags;
+
+    g_clear_pointer(&dnode->node, gsk_render_node_unref);
+    dnode->dnode_flags |= DRAW_NODE_DIRTY;
+
+    return (flags & DRAW_NODE_NOINK) && !(flags & (DRAW_NODE_UNDER))
+       && flags & DRAW_NODE_NOBG;
+}
+
+    static DrawNode *
+draw_node_copy(DrawNode *dnode)
+{
+    DrawNode *copy = draw_node_new(
+           dnode->font, dnode->glyphs, dnode->n_glyphs,
+           &dnode->bg_color, &dnode->fg_color, &dnode->sp_color,
+           dnode->flags, dnode->start_col, dnode->n_cells
+           );
+
+    // "copy" should never be NULL, so we don't need to check for NULL.
+    if (unlikely(dnode->dnode_flags & DRAW_NODE_CLIP))
+       copy->dnode_flags |= DRAW_NODE_CLIP;
+
+    return copy;
+}
+
+/*
+ * Split the draw node at the given cell offset in place (exclusive). If
+ * "keep_left" is TRUE, then keep the left halve (discard right halve), and 
vice
+ * versa. This will dirty the draw node.
+ *
+ * Returns TRUE if the new split draw node is not necessary anymore (see
+ * draw_node_new()), otherwise FALSE.
+ */
+    static gboolean
+draw_node_split(DrawNode *dnode, int cell_offset, gboolean keep_left)
+{
+    int                glyph_offset;
+    gboolean   split = TRUE;
+    gboolean   clip = FALSE;
+
+    glyph_offset = cell_offset_to_glyph(dnode->glyphs,
+           dnode->n_glyphs, cell_offset);
+
+    // Some fonts emulate ligatures by having spacer glyphs followed by a glyph
+    // that contains all the ink. If we tried splitting this type of ligature,
+    // then one side will incorrectly be empty.
+    //
+    // To handle this case, always clip the draw node so that the extra ink 
does
+    // not bleed out. If we are keeping the left side, then do not split,
+    // because we want to keep the glyph with all the ink. If we are keeping 
the
+    // right side, then we can split because the glyph with the ink will be on
+    // the right side always anyways.
+    for (int i = glyph_offset; i < dnode->n_glyphs; i++)
+    {
+       PangoRectangle ink;
+
+       pango_font_get_glyph_extents(dnode->font, dnode->glyphs[i].glyph,
+               &ink, NULL);
+
+       if (HAS_INK(&ink))
+       {
+           if (ink.x < 0)
+           {
+               split = !keep_left;
+               clip = TRUE;
+           }
+           break;
+       }
+    }
+
+    if (unlikely(clip))
+       dnode->dnode_flags |= DRAW_NODE_CLIP;
+
+    if (keep_left)
+    {
+       if (likely(split))
+           dnode->n_glyphs = glyph_offset;
+       dnode->n_cells = cell_offset;
+    }
+    else
+    {
+       if (likely(split))
+       {
+           // If this results in zero, then glyphs_has_ink() will return FALSE
+           // so it is fine.
+           dnode->n_glyphs -= glyph_offset;
+           // Shift glyphs after offset to beginning
+           memmove(dnode->glyphs, dnode->glyphs + glyph_offset,
+                   sizeof(PangoGlyphInfo) * dnode->n_glyphs);
+       }
+
+       dnode->n_cells -= cell_offset;
+       dnode->start_col += cell_offset;
+    }
+
+    if (likely(split))
+    {
+       dnode->glyphs = glyphs_resize(dnode->glyphs, dnode->n_glyphs);
+
+       // Recheck if new split glyphs has ink
+       if (glyphs_has_ink(dnode->font, dnode->glyphs, dnode->n_glyphs))
+           dnode->dnode_flags &= ~DRAW_NODE_NOINK;
+       else
+           dnode->dnode_flags |= DRAW_NODE_NOINK;
+    }
+
+    return draw_node_make_dirty(dnode);
+}
+
+/*
+ * If "dnode" is dirty, create a new render node for it at the given row and
+ * store it, then undirty it.
+ */
+    static void
+draw_node_render(DrawNode *dnode, int row, VimDrawArea *da)
+{
+    GskRenderNode      *nodes[3];
+    int                        n_nodes = 0;
+    GskRenderNode      *decor_node;
+
+    if (!(dnode->dnode_flags & DRAW_NODE_DIRTY))
+       return;
+
+    if (!(dnode->dnode_flags & DRAW_NODE_NOBG))
+    {
+       int width = dnode->n_cells * gui.char_width;
+       int bleed = gtk_widget_get_width(GTK_WIDGET(da)) - FILL_X(da->n_cols);
+
+       // If this draw node touches the end of the draw area. Bleed its
+       // background to the right if the space the draw area covers is slightly
+       // bigger than its actual visible area (that all cells cover). This just
+       // makes things like status bars look a bit nicer
+       if (END_COL(dnode) == da->n_cols - 1 && bleed > 0)
+           width += bleed;
+
+       nodes[n_nodes++] = gsk_color_node_new(&dnode->bg_color,
+               &GRAPHENE_RECT_INIT(FILL_X(dnode->start_col), FILL_Y(row),
+                   width, gui.char_height));
+    }
+
+    if (!(dnode->dnode_flags & DRAW_NODE_NOINK))
+    {
+       GskRenderNode       *text_node;
+       PangoGlyphString    glyphs_str;
+
+       // gsk_text_node_new() only uses the "glyphs" field, don't need to worry
+       // about the "log_clusters" array.
+       glyphs_str.glyphs = dnode->glyphs;
+       glyphs_str.num_glyphs = dnode->n_glyphs;
+       text_node = gsk_text_node_new(dnode->font, &glyphs_str, 
&dnode->fg_color,
+               &GRAPHENE_POINT_INIT(TEXT_X(dnode->start_col), TEXT_Y(row)));
+       // Should never be NULL since we check beforehand if there is ink.
+       assert(text_node != NULL);
+
+       if (dnode->dnode_flags & DRAW_NODE_CLIP)
+       {
+           GskRenderNode *old = text_node;
+
+           text_node = gsk_clip_node_new(text_node,
+                   &GRAPHENE_RECT_INIT(FILL_X(dnode->start_col), FILL_Y(row),
+                       dnode->n_cells * gui.char_width, gui.char_height));
+           gsk_render_node_unref(old);
+           assert(text_node != NULL);
+       }
+
+       nodes[n_nodes++] = text_node;
+    }
+
+    decor_node = create_under_decor_node(row, dnode->start_col, dnode->n_cells,
+           dnode->flags, &dnode->fg_color, &dnode->sp_color);
+    if (decor_node != NULL)
+       nodes[n_nodes++] = decor_node;
+
+    // Should never be zero
+    assert(n_nodes > 0);
+
+    if (likely(n_nodes == 1))
+       dnode->node = nodes[0];
+    else
+    {
+       dnode->node = gsk_container_node_new(nodes, n_nodes);
+       // gsk_container_node_new() takes its own reference
+       for (int i = 0; i < n_nodes; i++)
+           gsk_render_node_unref(nodes[i]);
+    }
+
+    dnode->dnode_flags &= ~DRAW_NODE_DIRTY;
+}
+
+/*
+ * Returns true if "dnode" matches "font" + "flags" in terms of
+ * color/visual attributes.
+ */
+    static gboolean
+draw_node_match(DrawNode *dnode, PangoFont *font, int flags)
+{
+    if (dnode->flags != flags)
+       return FALSE;
+
+    if (!(flags & DRAW_TRANSP)
+           && !gdk_rgba_equal(&dnode->bg_color, gui.bgcolor))
+       return FALSE;
+
+    if (!gdk_rgba_equal(&dnode->fg_color, gui.fgcolor))
+       return FALSE;
+
+    // Special color is only used for undercurls
+    if (flags & DRAW_UNDERC && !gdk_rgba_equal(&dnode->sp_color, gui.spcolor))
+       return FALSE;
+
+    // This may not work all the time, but creating two PangoFontDescription
+    // each time to compare equality seems slow...
+    return dnode->font == font;
+}
+
+/*
+ * Append or prepend the given glyphs to the draw node. If "start" is TRUE, 
then
+ * prepend, otherwise append. This will invalidate the draw node. Note that
+ * prepending does not update "start_col" or "n_cells".
+ */
+    static void
+draw_node_extend(
+       DrawNode                *dnode,
+       const PangoGlyphInfo    *glyphs,
+       int                     n_glyphs,
+       bool                    start)
+{
+    dnode->glyphs = glyphs_resize(dnode->glyphs, dnode->n_glyphs + n_glyphs);
+
+    if (start)
+    {
+       // Move the existing glyphs forward first
+       memmove(dnode->glyphs + n_glyphs, dnode->glyphs,
+               dnode->n_glyphs * sizeof(PangoGlyphInfo));
+       memcpy(dnode->glyphs, glyphs, n_glyphs * sizeof(PangoGlyphInfo));
+    }
+    else
+       memcpy(dnode->glyphs + dnode->n_glyphs, glyphs,
+               n_glyphs * sizeof(PangoGlyphInfo));
+
+    dnode->n_glyphs += n_glyphs;
+
+    if (glyphs_has_ink(dnode->font, dnode->glyphs, dnode->n_glyphs))
+       dnode->dnode_flags &= ~DRAW_NODE_NOINK;
+    else
+       dnode->dnode_flags |= DRAW_NODE_NOINK;
+    (void)draw_node_make_dirty(dnode);
+}
+
+/*
+ * Set the given cell to the draw node (which may be NULL), adding a new
+ * reference to it.
+ */
+    static void
+draw_cell_set(DrawCell *dcell, DrawNode *dnode)
+{
+    draw_node_unref(dcell->dnode);
+    dcell->dnode = dnode == NULL ? NULL : draw_node_ref(dnode);
+    dcell->invert = FALSE;
+}
+
+/*
+ * Set the cells between "col1" and "col2" (inclusive) to "dnode" (which may be
+ * NULL).
+ */
+    static void
+draw_row_fill(DrawCell *drow, int col1, int col2, DrawNode *dnode)
+{
+    for (int c = col1; c <= col2; c++)
+       draw_cell_set(drow + c, dnode);
+}
+
+/*
+ * Same as draw_row_fill(), but also handle truncating/splitting any draw nodes
+ * that overlap onto the set region. If "split" is TRUE, then only
+ * truncating/splitting is done.
+ *
+ * If "copy" is TRUE, then "dnode" is ignored and instead any draw nodes in the
+ * region that overlap outside of it are copied and clipped in addition to
+ * truncating draw nodes outside the region.
+ */
+    static void
+draw_row_set(
+       DrawCell    *drow,
+       int         col1,
+       int         col2,
+       DrawNode    *dnode,
+       gboolean    copy,
+       gboolean    split)
+{
+    DrawNode   *ldnode = drow[col1].dnode;
+    DrawNode   *rdnode = drow[col2].dnode;
+    DrawNode   *new_dnode = NULL;
+
+    if (ldnode != NULL && ldnode == rdnode
+           && (ldnode->start_col != col1 || END_COL(ldnode) > col2))
+    {
+       // Region in completely inside a single draw node. Truncate the existing
+       // draw node, and create a new draw node to be used as the right split.
+       if (END_COL(ldnode) > col2)
+       {
+           rdnode = draw_node_copy(ldnode);
+           draw_row_fill(drow, col2 + 1, END_COL(rdnode), rdnode);
+           draw_node_unref(rdnode);
+       }
+       else
+           // "ldnode" does not extend past "col2", no point in creating a new
+           // draw node on the right.
+           rdnode = NULL;
+
+       if (copy)
+           // Make another copy for the new draw node inside the set region.
+           // Must fill it in the row after, since "ldnode" may be unreferenced
+           // fully.
+           new_dnode = draw_node_copy(ldnode);
+    }
+
+    if (ldnode != NULL && ldnode->start_col != col1)
+    {
+       if (copy && new_dnode == NULL)
+       {
+           // Make a copy for the right halve.
+           DrawNode *new_right = draw_node_copy(ldnode);
+
+           if (draw_node_split(new_right,  col1 - ldnode->start_col, FALSE))
+               g_clear_pointer(&new_right, draw_node_unref);
+           draw_row_fill(drow, col1, END_COL(ldnode), new_right);
+           draw_node_unref(new_right);
+       }
+
+       // Leftmost draw node overlaps onto region, split it and discard right
+       // halve.
+       if (draw_node_split(ldnode, col1 - ldnode->start_col, TRUE))
+           // Draw node is not necessary anymore, clear it from the row.
+           draw_row_fill(drow, ldnode->start_col, col1 - 1, NULL);
+    }
+    if (rdnode != NULL && END_COL(rdnode) > col2)
+    {
+       if (copy && new_dnode == NULL)
+       {
+           // Make a copy for the left halve.
+           DrawNode *new_left = draw_node_copy(rdnode);
+
+           if (draw_node_split(new_left,  col2 - rdnode->start_col + 1, TRUE))
+               g_clear_pointer(&new_left, draw_node_unref);
+           draw_row_fill(drow, rdnode->start_col, col2, new_left);
+           draw_node_unref(new_left);
+       }
+
+       // Rightmost draw node overlaps onto region, split it and discard left
+       // halve.
+       if (draw_node_split(rdnode, col2 - rdnode->start_col + 1, FALSE))
+           draw_row_fill(drow, col2 + 1, END_COL(rdnode), NULL);
+    }
+
+    if (copy)
+    {
+       if (new_dnode != NULL)
+       {
+           if (draw_node_split(new_dnode, col1 - new_dnode->start_col, FALSE)
+                   || draw_node_split(new_dnode,
+                       col2 - new_dnode->start_col + 1, TRUE))
+               g_clear_pointer(&new_dnode, draw_node_unref);
+
+           draw_row_fill(drow, col1, col2, new_dnode);
+           draw_node_unref(new_dnode);
+       }
+    }
+    else if (!split)
+       draw_row_fill(drow, col1, col2, dnode);
+}
+
+/*
+ * Move the cells between "col1" and "col2" from "src" to "dest", overwriting
+ * the existing cells. This will handle clipping any draw nodes.
+ */
+    static void
+draw_row_move_to(DrawCell *dest_row, DrawCell *src_row, int col1, int col2)
+{
+    int move_size = (col2 - col1 + 1) * sizeof(DrawCell);
+
+    // Make sure that we free/truncate any draw nodes before we overwrite
+    // them.
+    draw_row_set(dest_row, col1, col2, NULL, FALSE, FALSE);
+
+    // Make sure that draw nodes at the "col1" and "col2" of "src_row" are
+    // clipped so that they all fit in the region being moved.
+    draw_row_set(src_row, col1, col2, NULL, TRUE, FALSE);
+
+    memmove(dest_row + col1, src_row + col1, move_size);
+
+    // Dirty the moved cells
+    for (int c = col1; c <= col2;)
+       if (dest_row[c].dnode != NULL)
+       {
+           (void)draw_node_make_dirty(dest_row[c].dnode);
+           c += dest_row[c].dnode->n_cells;
+       }
+       else
+           c++;
+
+    // NULL the draw nodes so we don't double unreference.
+    memset(src_row + col1, 0, (col2 - col1 + 1) * sizeof(DrawCell));
+}
+
+/*
+ * Should be called after modifying draw nodes within the given region.
+ */
+static void
+vim_draw_area_check_bounds(
+       VimDrawArea *self,
+       int         row1,
+       int         row2,
+       int         col1,
+       int         col2)
+{
+#if defined(FEAT_SIGN_ICONS) || defined(FEAT_NETBEANS_INTG)
+    graphene_rect_t bounds = GRAPHENE_RECT_INIT(
+           FILL_X(col1), FILL_Y(row1),
+           gui.char_width * (col2 - col1 + 1),
+           gui.char_height * (row2 - row1 + 1));
+#endif
+
+    if (self->cursor_node != NULL)
+       // Check if cursor node is within the the updated region. If so, then
+       // remove the render node. This only applies to the part and hollow
+       // cursor, the block cursor will be cleared in draw_row_make_space().
+       if (gui.row >= row1 && gui.row <= row2
+               && gui.col >= col1 && gui.col <= col2)
+           g_clear_pointer(&self->cursor_node, gsk_render_node_unref);
+
+#ifdef FEAT_SIGN_ICONS
+    // Clear any sign icons within the modified block if any
+    for (GList *s = self->signs->head; s != NULL;)
+    {
+       GList           *next = s->next;
+       graphene_rect_t rect;
+
+       gsk_render_node_get_bounds(s->data, &rect);
+
+       if (graphene_rect_contains_rect(&bounds, &rect))
+       {
+           // Keep going in case there are multiple sign icons within this
+           // block.
+           gsk_render_node_unref(s->data);
+           g_queue_delete_link(self->signs, s);
+       }
+       s = next;
+    }
+#endif
+#ifdef FEAT_NETBEANS_INTG
+    // Remove multi sign indicator if it is within the modified region.
+    if (self->multisign_node != NULL)
+    {
+       graphene_rect_t rect;
+
+       gsk_render_node_get_bounds(self->multisign_node, &rect);
+       if (graphene_rect_contains_rect(&bounds, &rect))
+           g_clear_pointer(&self->multisign_node, gsk_render_node_unref);
+    }
+#endif
+}
+
+/*
+ * Add the glyph string starting at column "col" in row "row". This will handle
+ * any background colours, fake bold, and under decorations. This does not 
queue
+ * a redraw for the widget.
+ */
+    void
+vim_draw_area_add_glyphs(
+       VimDrawArea *self,
+       int row,
+       int col,
+       int num_cells,
+       int flags,
+       PangoFont *font,
+       PangoGlyphString *glyphs)
+{
+    DrawCell   *drow;
+    DrawNode   *dnode = NULL;
+    int                end_col = col + num_cells - 1;
+
+    if (unlikely(self->cells == NULL
+               || row >= self->n_rows
+               || col >= self->n_cols
+               || col + num_cells > self->n_cols))
+       return;
+
+    drow = GET_ROW(self, row);
+
+    draw_row_set(drow, col, end_col, NULL, FALSE, TRUE);
+
+    // Check if leftmost draw node (if any) has the same visual
+    // attributes/colours as the glyph string being added. If so, then just
+    // extend that draw node with the new glyphs.
+    if (col > 0)
+    {
+       DrawNode *ldnode = drow[col - 1].dnode;
+
+       // Don't want to try merging draw nodes that are clipped, because the
+       // glyphs in them may not match one to one with the actual bounds of the
+       // draw node.
+       if (ldnode != NULL && !(ldnode->dnode_flags & DRAW_NODE_CLIP)
+               && draw_node_match(ldnode, font, flags))
+       {
+           draw_node_extend(ldnode, glyphs->glyphs, glyphs->num_glyphs, FALSE);
+           draw_row_fill(drow, col, end_col, ldnode);
+           ldnode->n_cells += num_cells;
+           dnode = ldnode;
+       }
+    }
+
+    // Check if we can use the existing draw node on the right. If so, then 
shift
+    // "rdnode" to the "col", and extend it. If we merged the left draw node, 
then
+    // instead extend it normally and unreference the right draw node.
+    if (col + num_cells < self->n_cols)
+    {
+       DrawNode *rdnode = drow[col + num_cells].dnode;
+
+       if (rdnode != NULL && !(rdnode->dnode_flags & DRAW_NODE_CLIP)
+               && draw_node_match(rdnode, font, flags))
+       {
+           if (dnode != NULL)
+           {
+               assert(rdnode->start_col == col + num_cells);
+               draw_node_extend(dnode, rdnode->glyphs, rdnode->n_glyphs, 
FALSE);
+               dnode->n_cells += rdnode->n_cells;
+               draw_row_fill(drow, rdnode->start_col, END_COL(rdnode), dnode);
+           }
+           else
+           {
+               draw_node_extend(rdnode, glyphs->glyphs, glyphs->num_glyphs, 
TRUE);
+               draw_row_fill(drow, col, end_col, rdnode);
+               rdnode->start_col = col;
+               rdnode->n_cells += num_cells;
+               dnode = rdnode;
+           }
+       }
+    }
+
+    if (dnode != NULL)
+       return;
+
+    dnode = draw_node_new(
+           font, glyphs->glyphs, glyphs->num_glyphs, gui.bgcolor,
+           gui.fgcolor, gui.spcolor, flags, col, num_cells
+           );
+    draw_row_fill(drow, col, end_col, dnode);
+    draw_node_unref(dnode);
+
+    vim_draw_area_check_bounds(self, row, row, col, col + num_cells - 1);
+}
+
+/*
+ * Clear out the block with the given bounds (inclusive).
+ */
+    void
+vim_draw_area_clear_block(
+       VimDrawArea     *self,
+       int             row1,
+       int             col1,
+       int             row2,
+       int             col2)
+{
+    if (unlikely(self->cells == NULL
+               || row1 >= self->n_rows
+               || col1 >= self->n_cols
+               || row2 >= self->n_rows
+               || col2 >= self->n_cols))
+       return;
+
+    for (int r = row1; r <= row2; r++)
+       draw_row_set(GET_ROW(self, r), col1, col2, NULL, FALSE, FALSE);
+
+    vim_draw_area_check_bounds(self, row1, row2, col1, col2);
+}
+
+/*
+ * Clear out the entire draw area
+ */
+    void
+vim_draw_area_clear(VimDrawArea *self)
+{
+    vim_draw_area_clear_block(self, 0, 0, self->n_rows - 1, self->n_cols - 1);
+}
+
+/*
+ * Move the given rows between "row1" and "row2", within the column "col1" and
+ * "col2" (making a rectangle region), to the row "to". The previous region 
that
+ * was moved is cleared.
+ */
+    void
+vim_draw_area_move_block(
+       VimDrawArea *self,
+       int         to,
+       int         row1,
+       int         row2,
+       int         col1,
+       int         col2)
+{
+
+    int                    offset = row2 - row1;
+#if defined(FEAT_SIGN_ICONS) || defined(FEAT_NETBEANS_INTG)
+    graphene_rect_t bounds = GRAPHENE_RECT_INIT(
+           FILL_X(col1), FILL_Y(row1),
+           gui.char_width * (col2 - col1 + 1),
+           gui.char_height * (row2 - row1 + 1));
+    graphene_rect_t clear_rect;
+#endif
+
+    if (unlikely(self->cells == NULL
+               || row1 >= self->n_rows
+               || row2 >= self->n_rows
+               || to >= self->n_rows
+               || col1 >= self->n_cols
+               || col2 >= self->n_cols))
+       return;
+
+    assert(row2 >= row1);
+    assert(col2 >= col1);
+    assert(row1 != to);
+
+    if (row1 > to)
+    {
+       // "row1" is below "to", start moving rows starting at "row1". Rows are
+       // being shifted upwards.
+       for (int o = 0; o <= offset; o++)
+           draw_row_move_to(GET_ROW(self, to + o), GET_ROW(self, row1 + o),
+                   col1, col2);
+    }
+    else
+    {
+       // "row1" is above "to", must start moving rows starting at "row2". Rows
+       // are being shifted downwards.
+       for (int o = offset; o >= 0; o--)
+           if (to + o >= self->n_rows)
+               // "src_row" is being "moved" off the screen, no need to move
+               // it physically.
+               gui_clear_block(row1 + o, col1, row1 + o, col2);
+           else
+               draw_row_move_to(GET_ROW(self, to + o), GET_ROW(self, row1 + o),
+                       col1, col2);
+    }
+
+    // Do not call vim_draw_area_check_bounds(), because we moved cells, not
+    // modified them.
+
+#if defined(FEAT_SIGN_ICONS) || defined(FEAT_NETBEANS_INTG)
+    if (row1 > to)
+       clear_rect = GRAPHENE_RECT_INIT(
+               FILL_X(col1), FILL_Y(to),
+               gui.char_width * (col2 - col1 + 1),
+               gui.char_height * (row1 - to));
+    else
+       clear_rect = GRAPHENE_RECT_INIT(
+               FILL_X(col1), FILL_Y(row2 + 1),
+               gui.char_width * (col2 - col1 + 1),
+               gui.char_height * (to - row1));
+#endif
+
+#ifdef FEAT_SIGN_ICONS
+    // Move sign icons if they are in the moved region
+    for (GList *s = self->signs->head; s != NULL;)
+    {
+       GList           *next = s->next;
+       GskRenderNode   *node = s->data;
+       graphene_rect_t  rect;
+
+       gsk_render_node_get_bounds(node, &rect);
+
+       // Check if icon moved off screen, if so then remove it.
+       if (graphene_rect_contains_rect(&clear_rect, &rect))
+       {
+           gsk_render_node_unref(s->data);
+           g_queue_delete_link(self->signs, s);
+           s = next;
+           continue;
+       }
+
+       if (graphene_rect_contains_rect(&bounds, &rect))
+       {
+           GdkTexture    *texture;
+           GskRenderNode *new;
+           float          new_y;
+
+           texture = gsk_texture_scale_node_get_texture(node);
+           new_y = graphene_rect_get_y(&rect) - graphene_rect_get_y(&bounds);
+           new_y += FILL_Y(to);
+
+           if (new_y >= 0 && new_y < gtk_widget_get_height(GTK_WIDGET(self)))
+           {
+               rect.origin.y = new_y;
+               new = gsk_texture_scale_node_new(texture, &rect,
+                       GSK_SCALING_FILTER_TRILINEAR);
+               gsk_render_node_unref(node);
+               s->data = new;
+           }
+           else
+           {
+               gsk_render_node_unref(s->data);
+               g_queue_delete_link(self->signs, s);
+           }
+       }
+       s = next;
+    }
+#endif
+#ifdef FEAT_NETBEANS_INTG
+    // Move multisign indicator node if needed
+    if (self->multisign_node != NULL)
+    {
+       graphene_rect_t rect;
+
+       gsk_render_node_get_bounds(self->multisign_node, &rect);
+
+       if (graphene_rect_contains_rect(&clear_rect, &rect))
+           g_clear_pointer(&self->multisign_node, gsk_render_node_unref);
+       else if (graphene_rect_contains_rect(&bounds, &rect))
+       {
+           float new_y =
+               graphene_rect_get_y(&rect) - graphene_rect_get_y(&bounds);
+
+           new_y += FILL_Y(to);
+
+           if (new_y >= 0 && new_y < gtk_widget_get_height(GTK_WIDGET(self)))
+           {
+               cairo_surface_t *surface;
+               GskRenderNode   *new;
+               cairo_t         *cr;
+
+               surface = gsk_cairo_node_get_surface(self->multisign_node);
+               rect.origin.y = new_y;
+               new = gsk_cairo_node_new(&rect);
+               cr = gsk_cairo_node_get_draw_context(new);
+               cairo_set_source_surface(cr, surface, 0, 0);
+               cairo_paint(cr);
+               cairo_destroy(cr);
+
+               gsk_render_node_unref(self->multisign_node);
+               self->multisign_node = new;
+           }
+           else
+               g_clear_pointer(&self->multisign_node, gsk_render_node_unref);
+       }
+    }
+#endif
+}
+
+/*
+ * Draw a hollow cursor at the cursor position using the current foreground
+ * color. Note that this does not queue a redraw
+ */
+    void
+vim_draw_area_set_hollow_cursor(VimDrawArea *self)
+{
+    GskRoundedRect     outline;
+    int                        i = 1;
+    static const float border[4] = {1.0f, 1.0f, 1.0f, 1.0f};
+    const GdkRGBA color[4] = {
+       *gui.fgcolor, *gui.fgcolor,
+       *gui.fgcolor, *gui.fgcolor
+    } ;
+
+    // Double cursor width if double width character
+    if (mb_lefthalve(gui.row, gui.col))
+       i = 2;
+
+    gsk_rounded_rect_init_from_rect(&outline,
+           &GRAPHENE_RECT_INIT(FILL_X(gui.col), FILL_Y(gui.row),
+               i * gui.char_width, gui.char_height),
+           0.0f);
+
+    node_unref(self->cursor_node);
+    self->cursor_node = gsk_border_node_new(&outline, border, color);
+}
+
+/*
+ * Draw a part cursor with width "w" and height "h". Note that this does not
+ * queue a redraw
+ */
+    void
+vim_draw_area_set_part_cursor(VimDrawArea *self, int w, int h)
+{
+    node_unref(self->cursor_node);
+    self->cursor_node = gsk_color_node_new(gui.fgcolor,
+           &GRAPHENE_RECT_INIT(
+#ifdef FEAT_RIGHTLEFT
+               CURSOR_BAR_RIGHT ? FILL_X(gui.col + 1) - w :
+#endif
+               FILL_X(gui.col), FILL_Y(gui.row) + gui.char_height - h,
+               w, h));
+}
+
+/*
+ * Invert the rectangle in the draw area.
+ */
+    void
+vim_draw_area_invert_block(
+       VimDrawArea     *self,
+       int             row,
+       int             col,
+       int             nrows,
+       int             ncols)
+{
+    if (unlikely(self->cells == NULL
+               || row >= self->n_rows
+               || col >= self->n_cols
+               || row + nrows - 1 >= self->n_rows
+               || col + ncols - 1 >= self->n_cols))
+       return;
+
+    for (int r = row; r < row + nrows; r++)
+    {
+       DrawCell *drow = GET_ROW(self, r);
+
+       for (int c = col; c < col + ncols; c++)
+       {
+           DrawCell *dcell = drow + c;
+
+           dcell->invert = !dcell->invert;
+       }
+    }
+}
+
+#if defined(FEAT_SIGN_ICONS)
+/*
+ * Add a sign texture at the given row and column, and scale it to "width" and
+ * "height".
+ */
+    void
+vim_draw_area_add_sign(
+       VimDrawArea *self,
+       GdkTexture *sign,
+       int row,
+       int col,
+       int width,
+       int height)
+{
+    GskRenderNode   *node;
+
+    if (unlikely(self->cells == NULL
+               || row >= self->n_rows
+               || col >= self->n_cols))
+       return;
+
+    node = gsk_texture_scale_node_new(sign,
+           &GRAPHENE_RECT_INIT(FILL_X(col), FILL_Y(row), width, height),
+           GSK_SCALING_FILTER_TRILINEAR);
+    if (node == NULL)
+       return;
+    g_queue_push_tail(self->signs, node);
+}
+#endif
+
+#ifdef FEAT_NETBEANS_INTG
+    cairo_t *
+vim_draw_area_get_multisign_cairo(VimDrawArea *self, int x, int y, int w, int 
h)
+{
+    node_unref(self->multisign_node);
+    self->multisign_node = gsk_cairo_node_new(
+           &GRAPHENE_RECT_INIT( x, y, w, h));
+    return gsk_cairo_node_get_draw_context(self->multisign_node);
+}
+#endif
+
+#ifdef FEAT_IMAGE_GDK
+
+/*
+ * Get the draw image with the given id, return NULL if not exists.
+ */
+    static GList *
+vim_draw_area_get_image(VimDrawArea *self, int id)
+{
+    for (GList *s = self->images->head; s != NULL; s = s->next)
+    {
+       DrawImage *sdimg = s->data;
+
+       if (sdimg->id == id)
+           return s;
+    }
+    return NULL;
+}
+
+/*
+ * Queue the given image to the correct position in the queue using its zindex.
+ */
+    static void
+vim_draw_area_queue_image(VimDrawArea *self, GList *link)
+{
+    DrawImage *dimg = link->data;
+
+    for (GList *s = self->images->head; s != NULL; s = s->next)
+    {
+       DrawImage *sdimg = s->data;
+
+       if (sdimg->zindex >= dimg->zindex)
+       {
+           g_queue_insert_before_link(self->images, s, link);
+           return;
+       }
+    }
+    // Queue is empty or image has new highest zindex
+    g_queue_push_tail_link(self->images, link);
+}
+
+/*
+ * Add an image at the given row and column with the specified zindex and id.
+ * (src_x, src_y, draw_w, draw_h) describe which pixel sub-rect of the source
+ * texture should be drawn. If there is an image that has the same id, then it
+ * is re-rendered with the new texture. If zindex of an image changed, then the
+ * queue will be updated accordingly.
+ */
+    void
+vim_draw_area_add_image(
+       VimDrawArea *self,
+       GdkTexture  *image,
+       int         row,
+       int         col,
+       int         src_x,
+       int         src_y,
+       int         draw_w,
+       int         draw_h,
+       int         zindex,
+       int         id)
+{
+    GskRenderNode   *node, *old;
+    int                    w, h;
+    graphene_rect_t clip;
+    GList          *link;
+    DrawImage      *dimg;
+
+    if (unlikely(self->cells == NULL
+               || row >= self->n_rows
+               || col >= self->n_cols))
+       return;
+
+    w = gdk_texture_get_width(image);
+    h = gdk_texture_get_height(image);
+
+    node = gsk_texture_node_new(image,
+           &GRAPHENE_RECT_INIT(FILL_X(col) - src_x, FILL_Y(row) - src_y,
+               w, h));
+
+    if (node != NULL)
+    {
+       graphene_rect_init(&clip, FILL_X(col), FILL_Y(row), draw_w, draw_h);
+
+       old = node;
+       node = gsk_clip_node_new(node, &clip);
+       gsk_render_node_unref(old);
+    }
+
+    link = vim_draw_area_get_image(self, id);
+    if (link == NULL)
+    {
+       dimg = g_new(DrawImage, 1);
+
+       dimg->id = id;
+       dimg->zindex = zindex;
+       dimg->node = node;
+
+       link = g_list_alloc();
+       link->data = dimg;
+    }
+    else
+    {
+       dimg = link->data;
+
+       gsk_render_node_unref(dimg->node);
+       dimg->node = node;
+
+       if (dimg->zindex == zindex)
+           return;
+       else
+       {
+           dimg->zindex = zindex;
+           g_queue_unlink(self->images, link);
+       }
+    }
+
+    vim_draw_area_queue_image(self, link);
+}
+
+    static void
+draw_image_free(DrawImage *dimg)
+{
+    gsk_render_node_unref(dimg->node);
+    g_free(dimg);
+}
+
+/*
+ * Remove the image with the given id if it exists
+ */
+    void
+vim_draw_area_remove_image(VimDrawArea *self, int id)
+{
+    GList *link = vim_draw_area_get_image(self, id);
+
+    if (link == NULL)
+       return;
+
+    draw_image_free(link->data);
+    g_queue_delete_link(self->images, link);
+}
+#endif
+
+    static void
+flush_invert_ga(garray_T *invert_ga, int row, int start, int len)
+{
+    if (ga_grow(invert_ga, 1) == OK)
+    {
+       graphene_rect_t *arr = (graphene_rect_t *)invert_ga->ga_data;
+
+       graphene_rect_init(arr + invert_ga->ga_len++,
+               FILL_X(start), FILL_Y(row),
+               len * gui.char_width, gui.char_height);
+    }
+}
+
+    static void
+vim_draw_area_snapshot(GtkWidget *widget, GtkSnapshot *snapshot)
+{
+    VimDrawArea                    *self = VIM_DRAW_AREA(widget);
+    int                            height, width;
+    static const GdkRGBA    white = {1, 1, 1, 1};
+    garray_T               invert_ga;
+
+    gui_mch_set_bg_color(gui.back_pixel);
+    height = gtk_widget_get_height(widget);
+    width = gtk_widget_get_width(widget);
+
+    if (self->cells == NULL)
+    {
+       gtk_snapshot_append_color(snapshot, gui.bgcolor,
+               &GRAPHENE_RECT_INIT(0, 0, width, height));
+       return;
+    }
+
+    // For inverted cells, we first build an array of bounds that represent
+    // blocks of inverted cells. Then we apply a white color to each of those
+    // bounds and then finish the blend.
+    gtk_snapshot_push_blend(snapshot, GSK_BLEND_MODE_DIFFERENCE);
+    ga_init2(&invert_ga, sizeof(graphene_rect_t), 8);
+
+    gtk_snapshot_append_color(snapshot, gui.bgcolor,
+           &GRAPHENE_RECT_INIT(0, 0, width, height));
+
+    for (int r = 0; r < self->n_rows; r++)
+    {
+       DrawCell    *drow = GET_ROW(self, r);
+       int         inv_len = 0;
+       int         inv_start;
+
+       for (int c = 0; c < self->n_cols; c++)
+       {
+           DrawCell    *dcell = drow + c;
+           DrawNode    *dnode = dcell->dnode;
+
+           // Batch inverted cells as single row rectangles.
+           if (dcell->invert)
+           {
+               if (inv_len == 0)
+                   inv_start = c;
+               inv_len++;
+           }
+           else if (!dcell->invert && inv_len > 0)
+           {
+               flush_invert_ga(&invert_ga, r, inv_start, inv_len);
+               inv_len = 0;
+           }
+
+           if (dnode == NULL)
+               continue;
+
+           if (dnode->start_col == c)
+           {
+               draw_node_render(dnode, r, self);
+               assert(dnode->node != NULL);
+               gtk_snapshot_append_node(snapshot, dnode->node);
+           }
+       }
+       // Flush trailing inverted blocks at end of row loop
+       if (inv_len > 0)
+           flush_invert_ga(&invert_ga, r, inv_start, inv_len);
+    }
+
+#ifdef FEAT_SIGN_ICONS
+    // Order of where the sign icon should be placed shouldn't matter,
+    // since caller will add whitespace padding in the region it covers.
+    // Probably should put it behind cursor though.
+    for (GList *s = self->signs->head; s != NULL; s = s->next)
+       gtk_snapshot_append_node(snapshot, s->data);
+#endif
+
+    if (self->cursor_node != NULL)
+       gtk_snapshot_append_node(snapshot, self->cursor_node);
+
+    gtk_snapshot_pop(snapshot);
+    for (int i = 0; i < invert_ga.ga_len; i++)
+    {
+       graphene_rect_t *rect = &((graphene_rect_t *)invert_ga.ga_data)[i];
+       gtk_snapshot_append_color(snapshot, &white, rect);
+    }
+    gtk_snapshot_pop(snapshot);
+    ga_clear(&invert_ga);
+
+#ifdef FEAT_IMAGE_GDK
+    // Draw images after any possible inversions
+    for (GList *s = self->images->head; s != NULL; s = s->next)
+    {
+       DrawImage *dimg = s->data;
+
+       if (dimg->node != NULL)
+           gtk_snapshot_append_node(snapshot, dimg->node);
+    }
+#endif
+}
+
+    static void
+vim_draw_area_size_allocate(
+       GtkWidget   *widget,
+       int         width,
+       int         height,
+       int         baseline UNUSED)
+{
+    VimDrawArea *self = VIM_DRAW_AREA(widget);
+    int                old_count = self->resize_count;
+
+    gui_resize_shell(width, height);
+
+    if (old_count == self->resize_count)
+    {
+       // Number of columns or rows hasn't changed. However still re render the
+       // draw nodes at the right edge of the draw area, so that they can
+       // update their background bleed (see draw_node_render()).
+       for (int r = 0; r < self->n_rows; r++)
+       {
+           DrawCell *dcell = &GET_ROW(self, r)[self->n_cols - 1];
+
+           if (dcell->dnode != NULL)
+           {
+               (void)draw_node_make_dirty(dcell->dnode);
+               draw_node_render(dcell->dnode, r, self);
+           }
+       }
+    }
+
+    return;
+}
+
+#endif // USE_GTK4_SNAPSHOT
diff --git a/src/gui_gtk4_da.h b/src/gui_gtk4_da.h
new file mode 100644
index 000000000..100d1dba0
--- /dev/null
+++ b/src/gui_gtk4_da.h
@@ -0,0 +1,38 @@
+/* vi:set ts=8 sts=4 sw=4 noet:
+ *
+ * VIM - Vi IMproved           by Bram Moolenaar
+ *
+ * Do ":help uganda"  in Vim to read copying and usage conditions.
+ * Do ":help credits" in Vim to see a list of people who contributed.
+ * See README.txt for an overview of the Vim source code.
+ */
+
+#ifndef GUI_GTK4_DRAW_AREA_H
+#define GUI_GTK4_DRAW_AREA_H
+
+#include "vim.h"
+
+#ifdef USE_GTK4_SNAPSHOT
+
+# include <gtk/gtk.h>
+
+# define VIM_TYPE_DRAW_AREA (vim_draw_area_get_type())
+G_DECLARE_FINAL_TYPE(VimDrawArea, vim_draw_area, VIM, DRAW_AREA, GtkWidget)
+
+GtkWidget *vim_draw_area_new(void);
+void vim_draw_area_set_size(VimDrawArea *self, int rows, int cols);
+void vim_draw_area_add_glyphs(VimDrawArea *self, int row, int col, int 
num_cells, int flags, PangoFont *font, PangoGlyphString *glyphs);
+void vim_draw_area_clear_block(VimDrawArea *self, int row1, int col1, int 
row2, int col2);
+void vim_draw_area_clear(VimDrawArea *self);
+void vim_draw_area_move_block(VimDrawArea *self, int to, int row1, int row2, 
int col1, int col2);
+void vim_draw_area_set_hollow_cursor(VimDrawArea *self);
+void vim_draw_area_set_part_cursor(VimDrawArea *self, int w, int h);
+void vim_draw_area_invert_block(VimDrawArea *self, int row, int col, int 
nrows, int ncols);
+void vim_draw_area_add_sign(VimDrawArea *self, GdkTexture *sign, int row, int 
col, int width, int height);
+cairo_t *vim_draw_area_get_multisign_cairo(VimDrawArea *self, int x, int y, 
int w, int h);
+void vim_draw_area_add_image(VimDrawArea *self, GdkTexture  *image, int row, 
int col, int src_x, int src_y, int draw_w, int draw_h, int zindex, int id);
+void vim_draw_area_remove_image(VimDrawArea *self, int id);
+
+#endif
+
+#endif
diff --git a/src/netbeans.c b/src/netbeans.c
index c7cb786ae..99fa9549c 100644
--- a/src/netbeans.c
+++ b/src/netbeans.c
@@ -3104,21 +3104,25 @@ netbeans_draw_multisign_indicator(int row)
     if (!NETBEANS_OPEN)
        return;
 
+    x = 0;
+    y = row * gui.char_height + 2;
+
 # if GTK_CHECK_VERSION(3,0,0)
+#  ifdef USE_GTK4_SNAPSHOT
+    cr = gui_gtk4_get_multisign_context(x, y, 5, gui.char_height);
+#  else
     cr = cairo_create(gui.surface);
+#  endif
     cairo_set_source_rgba(cr,
            gui.fgcolor->red, gui.fgcolor->green, gui.fgcolor->blue,
            gui.fgcolor->alpha);
 # endif
 
-    x = 0;
-    y = row * gui.char_height + 2;
-
     for (i = 0; i < gui.char_height - 3; i++)
 # if GTK_CHECK_VERSION(3,0,0)
        cairo_rectangle(cr, x+2, y++, 1, 1);
 # else
-       gdk_draw_point(drawable, gui.text_gc, x+2, y++);
+    gdk_draw_point(drawable, gui.text_gc, x+2, y++);
 # endif
 
 # if GTK_CHECK_VERSION(3,0,0)
diff --git a/src/po/vim.pot b/src/po/vim.pot
index 00c192a62..d21055030 100644
--- a/src/po/vim.pot
+++ b/src/po/vim.pot
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: Vim
"
 "Report-Msgid-Bugs-To: [email protected]
"
-"POT-Creation-Date: 2026-05-23 19:49+0000
"
+"POT-Creation-Date: 2026-06-13 17:59+0000
"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>
"
 "Language-Team: LANGUAGE <[email protected]>
"
@@ -3460,6 +3460,9 @@ msgstr ""
 msgid "without GUI."
 msgstr ""
 
+msgid "with GTK4 GUI (hwaccel)."
+msgstr ""
+
 msgid "with GTK4 GUI."
 msgstr ""
 
diff --git a/src/popupwin.c b/src/popupwin.c
index 3d427b5a6..8e6090d1a 100644
--- a/src/popupwin.c
+++ b/src/popupwin.c
@@ -944,6 +944,10 @@ apply_general_options(win_T *wp, dict_T *dict)
            {
 # ifdef FEAT_IMAGE_KITTY
                popup_image_clear_kitty(wp);
+# endif
+# ifdef FEAT_IMAGE_GDK
+               if (gui.in_use)
+                   gui_gtk4_remove_image(wp);
 # endif
                VIM_CLEAR(wp->w_popup_image_data);
                wp->w_popup_image_w = 0;
@@ -959,7 +963,7 @@ apply_general_options(win_T *wp, dict_T *dict)
                wp->w_popup_image_seq_cells_h = 0;
                wp->w_popup_image_emit_valid = false;
 # endif
-# if defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO)
+# if defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO) || 
defined(FEAT_IMAGE_GDK)
 #  ifdef FEAT_GUI
                if (gui.in_use)
                    gui_mch_free_popup_image(wp);
@@ -1021,7 +1025,7 @@ apply_general_options(win_T *wp, dict_T *dict)
 # endif
                if (wp->w_popup_image_data != NULL)
                {
-# if defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO)
+# if defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO) || 
defined(FEAT_IMAGE_GDK)
                    bool updated_in_place = false;
 
 #  ifdef FEAT_GUI
@@ -2385,6 +2389,10 @@ popup_adjust_position(win_T *wp)
                    // Kitty placements need to be deleted explicitly before
                    // the popup goes hidden -- see popup_hide().
                    popup_image_clear_kitty(wp);
+#endif
+#ifdef FEAT_IMAGE_GDK
+                   if (gui.in_use)
+                       gui_gtk4_remove_image(wp);
 #endif
                    popup_hide_for_textprop(wp);
                    if (wp->w_winrow + popup_height(wp) >= cmdline_row)
@@ -4253,6 +4261,12 @@ popup_hide(win_T *wp)
     // a kitty placement persists until explicitly deleted -- send the
     // delete APC before hiding so the image goes away with the popup.
     popup_image_clear_kitty(wp);
+#endif
+#ifdef FEAT_IMAGE_GDK
+    if (gui.in_use)
+       // Same reason as above for kitty. GdkTexture's are retained and
+       // rendered until they are removed.
+       gui_gtk4_remove_image(wp);
 #endif
     wp->w_popup_flags |= POPF_HIDDEN;
     // Do not decrement b_nwindows, we still reference the buffer.
@@ -4453,6 +4467,10 @@ popup_free(win_T *wp)
 #ifdef FEAT_IMAGE_KITTY
     // Remove the kitty placement before win_free_popup() invalidates wp.
     popup_image_clear_kitty(wp);
+#endif
+#ifdef FEAT_IMAGE_GDK
+    if (gui.in_use)
+       gui_gtk4_remove_image(wp);
 #endif
     sign_undefine_by_name(popup_get_sign_name(wp), FALSE);
     wp->w_buffer->b_locked = FALSE;
@@ -6689,7 +6707,7 @@ fill_opacity_padding(
 }
 
 #ifdef FEAT_IMAGE
-# if defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO)
+# if defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO) || 
defined(FEAT_IMAGE_GDK)
 /*
  * Apply "clipwindow" cropping to a popup image about to be drawn by the GUI.
  * On entry "*row"/"*col" are the popup's logical content top-left (cell
@@ -6766,7 +6784,8 @@ popup_image_gui_clip(
  * popup intends to draw).
  */
 # if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY) \
-       || defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO)
+       || defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO) \
+       || defined(FEAT_IMAGE_GDK)
     static void
 popup_invalidate_prev_image_rect(win_T *wp, popup_clip_T *cl)
 {
@@ -6788,7 +6807,8 @@ popup_invalidate_prev_image_rect(win_T *wp, popup_clip_T 
*cl)
     // the invalidation when nothing about the destination rectangle has
     // changed, so a stationary popup doesn't churn through screen_fill+image
     // re-emit on every redraw cycle.
-#  if defined(FEAT_GUI) && (defined(FEAT_IMAGE_GDI) || 
defined(FEAT_IMAGE_CAIRO))
+#  if (defined(FEAT_GUI) && (defined(FEAT_IMAGE_GDI) || 
defined(FEAT_IMAGE_CAIRO))) \
+    || defined(FEAT_IMAGE_GDK)
     if (gui.in_use)
     {
        int src_x, src_y, draw_w, draw_h;
@@ -6806,7 +6826,8 @@ popup_invalidate_prev_image_rect(win_T *wp, popup_clip_T 
*cl)
     }
 #  endif
 #  if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY)
-#   if defined(FEAT_GUI) && (defined(FEAT_IMAGE_GDI) || 
defined(FEAT_IMAGE_CAIRO))
+#   if defined(FEAT_GUI) && (defined(FEAT_IMAGE_GDI) || 
defined(FEAT_IMAGE_CAIRO)) \
+    || defined(FEAT_IMAGE_GDK)
     else
 #   endif
     {
@@ -6871,7 +6892,7 @@ popup_emit_image(win_T *wp)
     row = wp->w_winrow + wp->w_popup_border[0] + wp->w_popup_padding[0];
     col = wp->w_wincol + wp->w_popup_border[3] + wp->w_popup_padding[3];
 
-# if defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO)
+# if defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO) || 
defined(FEAT_IMAGE_GDK)
     if (gui.in_use)
     {
        int src_x, src_y, draw_w, draw_h;
@@ -7200,7 +7221,8 @@ update_popups(void (*win_update)(win_T *wp))
        popup_compute_clip(wp, &cl);
 
 #if defined(FEAT_IMAGE) && (defined(FEAT_IMAGE_SIXEL) || 
defined(FEAT_IMAGE_KITTY) \
-               || defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO))
+               || defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO) \
+               || defined(FEAT_IMAGE_GDK))
        // Clear ScreenLines under the previous image-emit rectangle so the
        // body/padding/border draws below actually paint over the cells even
        // when the desired char+attr matches what was already there.  See the
diff --git a/src/proto/gui_gtk4.pro b/src/proto/gui_gtk4.pro
index fc386d065..8c4c2ac41 100644
--- a/src/proto/gui_gtk4.pro
+++ b/src/proto/gui_gtk4.pro
@@ -38,8 +38,11 @@ void gui_mch_set_fg_color(guicolor_T color);
 void gui_mch_set_bg_color(guicolor_T color);
 void gui_mch_set_sp_color(guicolor_T color);
 guicolor_T gui_mch_get_rgb(guicolor_T pixel);
+void gui_gtk4_update_size(void);
+cairo_t *gui_gtk4_get_multisign_context(int x, int y, int w, int h);
 void gui_mch_clear_block(int row1, int col1, int row2, int col2);
 void gui_mch_clear_all(void);
+void gui_gtk4_remove_image(win_T *wp);
 void gui_mch_free_popup_image(win_T *wp);
 bool gui_mch_update_popup_image_pixels(win_T *wp);
 void gui_mch_draw_popup_image(win_T *wp, int row, int col, int src_x, int 
src_y, int draw_w, int draw_h);
diff --git a/src/structs.h b/src/structs.h
index 8218a80f5..c831e3341 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -4296,6 +4296,10 @@ struct window_S
     // structs.h does not have to pull in <cairo.h>.
     void       *w_popup_image_surface;
 #  endif
+#  ifdef FEAT_IMAGE_GDK
+    // Cached GdkTexture for the image.
+    void       *w_popup_image_texture;
+#  endif
 # endif
 # if defined(FEAT_TIMERS)
     timer_T    *w_popup_timer;     // timer for closing popup window
diff --git a/src/version.c b/src/version.c
index fbf3f5a3f..5e1648127 100644
--- a/src/version.c
+++ b/src/version.c
@@ -560,6 +560,11 @@ static char *(features[]) =
 #else
        "-image_cairo",
 #endif
+#ifdef FEAT_IMAGE_GDK
+       "+image_gdk",
+#else
+       "-image_gdk",
+#endif
 #ifdef FEAT_SOUND
        "+sound",
 #else
@@ -754,6 +759,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    632,
 /**/
     631,
 /**/
@@ -2365,7 +2372,11 @@ list_version(void)
     msg_puts(_("without GUI."));
 #elif defined(FEAT_GUI_GTK)
 # if defined(USE_GTK4)
+#  ifdef USE_GTK4_SNAPSHOT
+    msg_puts(_("with GTK4 GUI (hwaccel)."));
+#  else
     msg_puts(_("with GTK4 GUI."));
+#  endif
 # elif defined(USE_GTK3)
     msg_puts(_("with GTK3 GUI."));
 # elif defined(FEAT_GUI_GNOME)
diff --git a/src/window.c b/src/window.c
index 2d18f053b..89c890f0f 100644
--- a/src/window.c
+++ b/src/window.c
@@ -6217,7 +6217,8 @@ win_free_popup(win_T *win)
 #  ifdef FEAT_IMAGE_SIXEL
     vim_free(win->w_popup_image_seq);
 #  endif
-#  if defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO)
+#  if defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO) \
+    || defined(FEAT_IMAGE_GDK)
     gui_mch_free_popup_image(win);
 #  endif
 # endif

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1wYSsr-00DOJ7-L3%40256bit.org.

Raspunde prin e-mail lui