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.