From: Foxe Chen Date: Sat, 13 Jun 2026 17:49:03 +0000 (+0000) Subject: patch 9.2.0632: GTK4: no support for hardware-accelerated rendering X-Git-Tag: v9.2.0632^0 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=c1e0c8edf2402dd2ffb3e261a3d67356f73e6467;p=thirdparty%2Fvim.git 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 Signed-off-by: Christian Brabandt --- diff --git a/Filelist b/Filelist index 33e07d5d36..5526e539a8 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 caad78ceba..06c258eafe 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 5f8056beba..2e558d8a0a 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 a06924b999..cf4e9e2543 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 086af1973c..88fad5440f 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 cc3ef1075b..4a4bd69554 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 b78a3738a6..df216fae19 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 e8b8be8e5b..1a6fa1c66b 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 17813c5dbc..483beff826 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\n" "$enable_gtk4_check" >&6; } fi fi +if test "x$SKIP_GTK4" != "xYES" -a "$enable_gui_canon" = "gtk4"; then + { printf "%s\n" "$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\n" "$as_me:${as_lineno-$LINENO}: result: $enable_gtk4_hwaccel" >&5 +printf "%s\n" "$enable_gtk4_hwaccel" >&6; } + + if test "x$enable_gtk4_hwaccel" = "xyes"; then + printf "%s\n" "#define USE_GTK4_SNAPSHOT 1" >>confdefs.h + + fi +fi + + if test "x$SKIP_GTK3" != "xYES" -a "$enable_gui_canon" != "gtk3"; then { printf "%s\n" "$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 e4273f5b7c..7861329897 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 cf6a59f6ca..06f03344cd 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 80e2a63392..45002c5bf8 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 59cd7bb485..3a0b6931c0 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 680f90fd3a..406d804252 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 771e5d307e..674601e0f7 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 dd4946793d..77ac056366 100644 --- a/src/gui_gtk4.c +++ b/src/gui_gtk4.c @@ -30,6 +30,9 @@ #include #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 0000000000..34dad93a61 --- /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 +#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 0000000000..100d1dba06 --- /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 + +# 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 c7cb786ae8..99fa9549c2 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 00c192a621..d210550308 100644 --- a/src/po/vim.pot +++ b/src/po/vim.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Vim\n" "Report-Msgid-Bugs-To: vim-dev@vim.org\n" -"POT-Creation-Date: 2026-05-23 19:49+0000\n" +"POT-Creation-Date: 2026-06-13 17:59+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -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 3d427b5a63..8e6090d1a4 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 fc386d0658..8c4c2ac415 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 8218a80f5b..c831e3341f 100644 --- a/src/structs.h +++ b/src/structs.h @@ -4296,6 +4296,10 @@ struct window_S // structs.h does not have to pull in . 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 fbf3f5a3fe..5e1648127c 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 2d18f053b6..89c890f0f4 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