]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0501: GTK4: there is no GTK4 UI available v9.2.0501
authorYasuhiro Matsumoto <mattn.jp@gmail.com>
Tue, 19 May 2026 18:13:27 +0000 (18:13 +0000)
committerChristian Brabandt <cb@256bit.org>
Tue, 19 May 2026 18:20:19 +0000 (18:20 +0000)
Problem:  GTK4: there is no GTK4 UI available
Solution: Implement GTK4 UI (Yasuhiro Matsumoto).

To enable, use the --enable-gui=gtk4 configure switch. Configure
currently favors GTK3 over GTK4 if no explicit --enable-gui switch has
been given and both libraries are present

closes: #19815

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Yasuhiro Matsumoto <mattn.jp@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
25 files changed:
.github/actions/build_vim_on_linux/action.yml
Filelist
src/INSTALL
src/Makefile
src/auto/configure
src/beval.h
src/clipboard.c
src/config.h.in
src/configure.ac
src/evalfunc.c
src/feature.h
src/getchar.c
src/gui.c
src/gui.h
src/gui_beval.c
src/gui_gtk4.c [new file with mode: 0644]
src/gui_gtk4_f.c [new file with mode: 0644]
src/gui_gtk4_f.h [new file with mode: 0644]
src/gui_xim.c
src/hardcopy.c
src/po/vim.pot
src/proto.h
src/proto/gui_gtk4.pro [new file with mode: 0644]
src/version.c
src/vim.h

index 9a2105164bee8f94ffc58504c2449415e73201a6..c6486cd34f066a8c9c9d4e38bba7cfbb1cb34506 100644 (file)
@@ -88,6 +88,7 @@ runs:
         sudo cp ci/pinned-pkgs /etc/apt/preferences.d/pinned-pkgs
         echo '::endgroup::'
 
+    # TODO: switch to GTK4 GUI
     - name: Install packages
       shell: bash
       run: |
index ceb06739b95869ffe3686db56ba0a062c2ad7978..dfc828a759fe561220beecb7d8d2cb38abdc4c84 100644 (file)
--- a/Filelist
+++ b/Filelist
@@ -498,6 +498,9 @@ SRC_UNIX =  \
                src/gui_gtk_f.c \
                src/gui_gtk_f.h \
                src/gui_gtk_x11.c \
+               src/gui_gtk4.c \
+               src/gui_gtk4_f.c \
+               src/gui_gtk4_f.h \
                src/gui_gtk_res.xml \
                src/gui_motif.c \
                src/gui_xmdlg.c \
@@ -527,6 +530,7 @@ SRC_UNIX =  \
                src/proto/gui_gtk.pro \
                src/proto/gui_gtk_x11.pro \
                src/proto/gui_gtk_gresources.pro \
+               src/proto/gui_gtk4.pro \
                src/proto/gui_motif.pro \
                src/proto/gui_xmdlg.pro \
                src/proto/gui_x11.pro \
index 083780cc82c2db996f3c564bef1cd073fd723cd3..96f32607fc90d6bc71cdc9a5fb06926b7d622070 100644 (file)
@@ -65,7 +65,11 @@ To build Vim on Ubuntu from scratch on a clean system using git:
        % sudo apt install libwayland-dev
        % make reconfig
 
-       Add GUI support:
+       Add GUI (GTK3) support:
+       % sudo apt install libgtk-3-dev
+       % make reconfig
+
+       Add GUI (GTK4) support:
        % sudo apt install libgtk-3-dev
        % make reconfig
 
index 97200a6b07906abb8ff01f53bea0c31e151c8c3c..e08b6cccfd138964df8b3ee49898b7192fc8f48b 100644 (file)
@@ -1232,6 +1232,23 @@ GTK_MAN_TARGETS = yes
 GTK_TESTTARGET  = gui
 GTK_BUNDLE     =
 
+### GTK4 GUI
+GTK4_SRC       = gui.c gui_gtk4.c gui_gtk4_f.c \
+                       $(GRESOURCE_SRC)
+GTK4_OBJ       = objects/gui.o objects/gui_gtk4.o \
+                       objects/gui_gtk4_f.o \
+                       $(GRESOURCE_OBJ)
+GTK4_DEFS      = -DFEAT_GUI_GTK $(NARROW_PROTO)
+GTK4_IPATH     = $(GUI_INC_LOC)
+GTK4_LIBS_DIR  = $(GUI_LIB_LOC)
+GTK4_LIBS1     =
+GTK4_LIBS2     = $(GTK_LIBNAME)
+GTK4_INSTALL   = install_normal install_gui_extra
+GTK4_TARGETS   = installglinks
+GTK4_MAN_TARGETS = yes
+GTK4_TESTTARGET        = gui
+GTK4_BUNDLE    =
+
 ### Motif GUI
 MOTIF_SRC      = gui.c gui_motif.c gui_x11.c gui_beval.c \
                        gui_xmdlg.c gui_xmebw.c
@@ -1289,8 +1306,8 @@ HAIKUGUI_TESTTARGET = gui
 HAIKUGUI_BUNDLE =
 
 # All GUI files
-ALL_GUI_SRC  = gui.c gui_gtk.c gui_gtk_f.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_motif.pro proto/gui_xmdlg.pro proto/gui_gtk_x11.pro proto/gui_x11.pro proto/gui_w32.pro proto/gui_photon.pro
+ALL_GUI_SRC  = gui.c gui_gtk.c gui_gtk_f.c gui_gtk4.c gui_gtk4_f.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
 
 # }}}
 
@@ -3364,6 +3381,13 @@ objects/gui_gtk_gresources.o: auto/gui_gtk_gresources.c
 objects/gui_gtk_x11.o: gui_gtk_x11.c
        $(CCC) -o $@ gui_gtk_x11.c
 
+objects/gui_gtk4.o: gui_gtk4.c
+       $(CCC) -o $@ gui_gtk4.c
+
+objects/gui_gtk4_f.o: gui_gtk4_f.c
+       $(CCC) -o $@ gui_gtk4_f.c
+
+
 objects/gui_haiku.o: gui_haiku.cc
        $(CCC) -o $@ gui_haiku.cc
 
@@ -4797,6 +4821,7 @@ proto/winclip.pro: winclip.c
 proto/window.pro: window.c
 proto/gui.pro: gui.c
 proto/gui_gtk.pro: gui_gtk.c
+proto/gui_gtk4.pro: gui_gtk4.c
 proto/gui_motif.pro: gui_motif.c
 proto/gui_xmdlg.pro: gui_xmdlg.c
 proto/gui_gtk_x11.pro: gui_gtk_x11.c
index 170552515683ca78af4d087b2a2ec9001978752d..ff4d5e361c76312bcd201b700a3487b8d06d62f3 100755 (executable)
@@ -862,6 +862,7 @@ with_x
 enable_gui
 enable_gtk2_check
 enable_gnome_check
+enable_gtk4_check
 enable_gtk3_check
 enable_motif_check
 enable_gtktest
@@ -1538,9 +1539,10 @@ Optional Features:
   --disable-farsi         Deprecated.
   --enable-xim            Include XIM input support.
   --enable-fontset        Include X fontset output support.
-  --enable-gui=OPTS       X11 GUI. default=auto OPTS=auto/no/gtk2/gnome2/gtk3/motif/haiku/photon/carbon
+  --enable-gui=OPTS       X11 GUI. default=auto OPTS=auto/no/gtk2/gnome2/gtk3/gtk4/motif/haiku/photon/carbon
   --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-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
@@ -10486,6 +10488,7 @@ enable_gui_canon=`echo "_$enable_gui" | \
 
 SKIP_GTK2=YES
 SKIP_GTK3=YES
+SKIP_GTK4=YES
 SKIP_GNOME=YES
 SKIP_MOTIF=YES
 SKIP_PHOTON=YES
@@ -10516,7 +10519,7 @@ printf "%s\n" "no GUI support" >&6; }
                SKIP_PHOTON=YES ;;
     yes|""|auto) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: automatic GUI support" >&5
 printf "%s\n" "automatic GUI support" >&6; }
-               gui_auto=yes ;;
+               gui_auto=yes ;;
     photon)    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Photon GUI support" >&5
 printf "%s\n" "Photon GUI support" >&6; } ;;
     *)         { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Sorry, $enable_gui GUI is not supported" >&5
@@ -10530,7 +10533,7 @@ elif test "x$MACOS_X" = "xyes" -a "x$with_x" = "xno" ; then
 printf "%s\n" "no GUI support" >&6; } ;;
     yes|"")    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes - automatic GUI support" >&5
 printf "%s\n" "yes - automatic GUI support" >&6; }
-               gui_auto=yes ;;
+               gui_auto=yes ;;
     auto)      { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: auto - disable GUI support for Mac OS" >&5
 printf "%s\n" "auto - disable GUI support for Mac OS" >&6; } ;;
     *)         { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Sorry, $enable_gui GUI is not supported" >&5
@@ -10546,6 +10549,7 @@ printf "%s\n" "yes/auto - automatic GUI support" >&6; }
                gui_auto=yes
                SKIP_GTK2=
                SKIP_GTK3=
+               SKIP_GTK4=
                SKIP_GNOME=
                SKIP_MOTIF=;;
     gtk2)      { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: GTK+ 2.x GUI support" >&5
@@ -10558,6 +10562,9 @@ printf "%s\n" "GNOME 2.x GUI support" >&6; }
     gtk3)      { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: GTK+ 3.x GUI support" >&5
 printf "%s\n" "GTK+ 3.x GUI support" >&6; }
                SKIP_GTK3=;;
+    gtk4)      { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: GTK 4.x GUI support" >&5
+printf "%s\n" "GTK 4.x GUI support" >&6; }
+               SKIP_GTK4=;;
     motif)     { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Motif GUI support" >&5
 printf "%s\n" "Motif GUI support" >&6; }
                SKIP_MOTIF=;;
@@ -10607,6 +10614,25 @@ printf "%s\n" "$enable_gnome_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 whether or not to look for GTK 4" >&5
+printf %s "checking whether or not to look for GTK 4... " >&6; }
+  # Check whether --enable-gtk4-check was given.
+if test ${enable_gtk4_check+y}
+then :
+  enableval=$enable_gtk4_check;
+else case e in #(
+  e) enable_gtk4_check="yes" ;;
+esac
+fi
+
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $enable_gtk4_check" >&5
+printf "%s\n" "$enable_gtk4_check" >&6; }
+  if test "x$enable_gtk4_check" = "xno"; then
+    SKIP_GTK4=YES
+  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; }
@@ -10681,6 +10707,8 @@ printf "%s\n" "gtk test disabled" >&6; }
     gtk_pkg_name="gtk+-2.0" ;; #(
   3.*) :
     gtk_pkg_name="gtk+-3.0" ;; #(
+  4.*) :
+    gtk_pkg_name="gtk4" ;; #(
   *) :
     { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5
 printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;}
@@ -10697,7 +10725,7 @@ then :
 printf "%s\n" "found" >&6; }
             { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GTK - version >= $min_gtk_version" >&5
 printf %s "checking for GTK - version >= $min_gtk_version... " >&6; }
-                                    GTK_CPPFLAGS=`$PKG_CONFIG --cflags-only-I $gtk_pkg_name`
+            GTK_CPPFLAGS=`$PKG_CONFIG --cflags-only-I $gtk_pkg_name`
             GTK_CFLAGS=`$PKG_CONFIG --cflags-only-other $gtk_pkg_name`
             GTK_LIBDIR=`$PKG_CONFIG --libs-only-L $gtk_pkg_name`
             GTK_LIBS=`$PKG_CONFIG --libs $gtk_pkg_name`
@@ -10821,6 +10849,7 @@ fi
 
     if test -n "$GTK_CPPFLAGS"; then
       SKIP_GTK2=YES
+      SKIP_GTK4=YES
       SKIP_GNOME=YES
       SKIP_MOTIF=YES
       GUITYPE=GTK
   fi
 fi
 
+if test -z "$SKIP_GTK4"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking --disable-gtktest argument" >&5
+printf %s "checking --disable-gtktest argument... " >&6; }
+  # Check whether --enable-gtktest was given.
+if test ${enable_gtktest+y}
+then :
+  enableval=$enable_gtktest;
+else case e in #(
+  e) enable_gtktest=yes ;;
+esac
+fi
+
+  if test "x$enable_gtktest" = "xyes" ; then
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: gtk test enabled" >&5
+printf "%s\n" "gtk test enabled" >&6; }
+  else
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: gtk test disabled" >&5
+printf "%s\n" "gtk test disabled" >&6; }
+  fi
+
+  if test "x$PKG_CONFIG" != "xno"; then
+
+      min_gtk_version="4.0.0"
+
+    if test "$PKG_CONFIG" != "no"; then
+    case $min_gtk_version in #(
+  2.*) :
+    gtk_pkg_name="gtk+-2.0" ;; #(
+  3.*) :
+    gtk_pkg_name="gtk+-3.0" ;; #(
+  4.*) :
+    gtk_pkg_name="gtk4" ;; #(
+  *) :
+    { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;}
+as_fn_error $? "The configure script does not know which pkg-config name to use for GTK $min_gtk_version\"
+See 'config.log' for more details" "$LINENO" 5; } ;;
+esac
+
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for pkg-config $gtk_pkg_name" >&5
+printf %s "checking for pkg-config $gtk_pkg_name... " >&6; }
+    if "$PKG_CONFIG" --exists "$gtk_pkg_name"
+then :
+
+            { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: found" >&5
+printf "%s\n" "found" >&6; }
+            { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GTK - version >= $min_gtk_version" >&5
+printf %s "checking for GTK - version >= $min_gtk_version... " >&6; }
+            GTK_CPPFLAGS=`$PKG_CONFIG --cflags-only-I $gtk_pkg_name`
+            GTK_CFLAGS=`$PKG_CONFIG --cflags-only-other $gtk_pkg_name`
+            GTK_LIBDIR=`$PKG_CONFIG --libs-only-L $gtk_pkg_name`
+            GTK_LIBS=`$PKG_CONFIG --libs $gtk_pkg_name`
+            gtk_major_version=`$PKG_CONFIG --modversion $gtk_pkg_name | \
+              sed 's/\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\1/'`
+            gtk_minor_version=`$PKG_CONFIG --modversion $gtk_pkg_name | \
+              sed 's/\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\2/'`
+            gtk_micro_version=`$PKG_CONFIG --modversion $gtk_pkg_name | \
+              sed 's/\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\3/'`
+            { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes; found version $gtk_major_version.$gtk_minor_version.$gtk_micro_version" >&5
+printf "%s\n" "yes; found version $gtk_major_version.$gtk_minor_version.$gtk_micro_version" >&6; }
+
+else case e in #(
+  e)
+             GTK_CPPFLAGS=""
+             GTK_CFLAGS=""
+             GTK_LIBDIR=""
+             GTK_LIBS=""
+             { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no; consider installing your distro GTK -dev package" >&5
+printf "%s\n" "no; consider installing your distro GTK -dev package" >&6; }
+             if test "$fail_if_missing" = "yes" -a "$gui_auto" != "yes"; then
+               as_fn_error $? "pkg-config could not find $gtk_pkg_name" "$LINENO" 5
+             fi
+            ;;
+esac
+fi
+  fi
+
+              gtktest_success="yes"
+  if test "$enable_gtktest" = "yes"; then
+  {
+    ac_save_CPPFLAGS="$CPPFLAGS"
+    ac_save_CFLAGS="$CFLAGS"
+    ac_save_LIBS="$LIBS"
+    CPPFLAGS="$CPPFLAGS $GTK_CPPFLAGS"
+    CFLAGS="$CFLAGS $GTK_CFLAGS"
+    LIBS="$LIBS $GTK_LIBS"
+
+                { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking ability to compile GTK test program" >&5
+printf %s "checking ability to compile GTK test program... " >&6; }
+    if test "$cross_compiling" = yes
+then :
+  echo $ac_n "cross compiling; assumed OK... $ac_c"
+else case e in #(
+  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+#include <gtk/gtk.h>
+#include <stdio.h>
+#if STDC_HEADERS
+# include <stdlib.h>
+# include <stddef.h>
+#endif
+
+int
+main ()
+{
+       int ex_major = $gtk_major_version;
+       int ex_minor = $gtk_minor_version;
+       int ex_micro = $gtk_micro_version;
+
+       #if $gtk_major_version == 2
+       guint ob_major = gtk_major_version;
+       guint ob_minor = gtk_minor_version;
+       guint ob_micro = gtk_micro_version;
+       #else
+       guint ob_major = gtk_get_major_version();
+       guint ob_minor = gtk_get_minor_version();
+       guint ob_micro = gtk_get_micro_version();
+       #endif
+
+       if ((ob_major > ex_major) ||
+               ((ob_major == ex_major)
+                       && (ob_minor > ex_minor)) ||
+               ((ob_major == ex_major)
+                       && (ob_minor == ex_minor)
+                       && (ob_micro >= ex_micro)))
+               return 0;
+       else
+               return 1;
+}
+
+_ACEOF
+if ac_fn_c_try_run "$LINENO"
+then :
+  gtktest_success="yes"; { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+else case e in #(
+  e) gtktest_success="no"; { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; } ;;
+esac
+fi
+rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \
+  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;
+esac
+fi
+
+    CPPFLAGS="$ac_save_CPPFLAGS"
+    CFLAGS="$ac_save_CFLAGS"
+    LIBS="$ac_save_LIBS"
+  }
+  fi
+
+  if test "$gtktest_success" = "yes"; then
+    GUI_LIB_LOC="$GTK_LIBDIR"
+                GTK_LIBNAME="$GTK_LIBS"
+                GUI_INC_LOC="$GTK_CPPFLAGS"
+  else
+        GTK_CPPFLAGS=""
+    GTK_CFLAGS=""
+    GTK_LIBDIR=""
+    GTK_LIBS=""
+    if test "$fail_if_missing" = "yes" -a "$gui_auto" != "yes"; then
+      as_fn_error $? "Failed to compile GTK test program." "$LINENO" 5
+    fi
+  fi
+
+
+
+
+
+    if test -n "$GTK_CPPFLAGS"; then
+      SKIP_GTK3=YES
+      SKIP_GTK2=YES
+      SKIP_GNOME=YES
+      SKIP_MOTIF=YES
+      GUITYPE=GTK4
+
+      printf "%s\n" "#define USE_GTK4 1" >>confdefs.h
+
+                        X_LIBS=
+      X_PRE_LIBS=
+      X_EXTRA_LIBS=
+      X_LIB=
+            if test "$enable_xim" = "auto"; then
+        enable_xim="yes"
+      fi
+    fi
+  fi
+fi
+
 if test -z "$SKIP_GTK2"; then
   { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking --disable-gtktest argument" >&5
 printf %s "checking --disable-gtktest argument... " >&6; }
@@ -10861,6 +11080,8 @@ printf "%s\n" "gtk test disabled" >&6; }
     gtk_pkg_name="gtk+-2.0" ;; #(
   3.*) :
     gtk_pkg_name="gtk+-3.0" ;; #(
+  4.*) :
+    gtk_pkg_name="gtk4" ;; #(
   *) :
     { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5
 printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;}
@@ -10877,7 +11098,7 @@ then :
 printf "%s\n" "found" >&6; }
             { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GTK - version >= $min_gtk_version" >&5
 printf %s "checking for GTK - version >= $min_gtk_version... " >&6; }
-                                    GTK_CPPFLAGS=`$PKG_CONFIG --cflags-only-I $gtk_pkg_name`
+            GTK_CPPFLAGS=`$PKG_CONFIG --cflags-only-I $gtk_pkg_name`
             GTK_CFLAGS=`$PKG_CONFIG --cflags-only-other $gtk_pkg_name`
             GTK_LIBDIR=`$PKG_CONFIG --libs-only-L $gtk_pkg_name`
             GTK_LIBS=`$PKG_CONFIG --libs $gtk_pkg_name`
index 694bec4dc52e968ad9ec1e9f8d6ee2f7bff7c349..4225e926d2d05c970833b44368e14ba9f4170c0c 100644 (file)
@@ -11,7 +11,7 @@
 #define BEVAL__H
 
 #ifdef FEAT_GUI_GTK
-# ifdef USE_GTK3
+# if defined(USE_GTK3) || defined(USE_GTK4)
 #  include <gtk/gtk.h>
 # else
 #  include <gtk/gtkwidget.h>
index 902f9d6032c4b244eacabda9803de95252214fa8..6f847edc1c3c01b02467924be07a37201c414373 100644 (file)
@@ -1891,7 +1891,8 @@ clip_x11_set_selection(Clipboard_T *cbd UNUSED)
 
 # endif
 
-# if defined(FEAT_XCLIPBOARD) || defined(FEAT_GUI_X11) || defined(FEAT_GUI_GTK)
+# if (defined(FEAT_XCLIPBOARD) || defined(FEAT_GUI_X11) || defined(FEAT_GUI_GTK)) \
+       && !defined(USE_GTK4)
 /*
  * Get the contents of the X CUT_BUFFER0 and put it in "cbd".
  */
@@ -3298,7 +3299,7 @@ did_set_clipboard(optset_T *args UNUSED)
        vim_regfree(clip_exclude_prog);
        clip_exclude_prog = new_exclude_prog;
 # endif
-# ifdef FEAT_GUI_GTK
+# if defined(FEAT_GUI_GTK) && !defined(USE_GTK4)
        if (gui.in_use)
        {
            gui_gtk_set_selection_targets((GdkAtom)GDK_SELECTION_PRIMARY);
index 699fde7ae002a1180e96c9c608c7a28504de3473..e4273f5b7cec8b266974e9eb22852fbbecdf5270 100644 (file)
 /* Define if GTK+ GUI is to be linked against GTK+ 3 */
 #undef USE_GTK3
 
+/* Define if GTK GUI is to be linked against GTK 4 */
+#undef USE_GTK4
+
 /* Define if we have isinf() */
 #undef HAVE_ISINF
 
index 5f79bb6a0ee6c1bcfb1d497b0bce7c07870cd258..a808530d240f33537c48240f8893bb14ee877e20 100644 (file)
@@ -2613,7 +2613,7 @@ test "x$with_x" = xno -a "x$HAIKU" != "xyes" -a "x$MACOS_X" != "xyes" -a "x$QNX"
 
 AC_MSG_CHECKING(--enable-gui argument)
 AC_ARG_ENABLE(gui,
- [  --enable-gui[=OPTS]       X11 GUI. [default=auto] [OPTS=auto/no/gtk2/gnome2/gtk3/motif/haiku/photon/carbon]], , enable_gui="auto")
+ [  --enable-gui[=OPTS]       X11 GUI. [default=auto] [OPTS=auto/no/gtk2/gnome2/gtk3/gtk4/motif/haiku/photon/carbon]], , enable_gui="auto")
 
 dnl Canonicalize the --enable-gui= argument so that it can be easily compared.
 dnl Do not use character classes for portability with old tools.
@@ -2623,6 +2623,7 @@ enable_gui_canon=`echo "_$enable_gui" | \
 dnl Skip everything by default.
 SKIP_GTK2=YES
 SKIP_GTK3=YES
+SKIP_GTK4=YES
 SKIP_GNOME=YES
 SKIP_MOTIF=YES
 SKIP_PHOTON=YES
@@ -2646,7 +2647,7 @@ elif test "x$QNX" = "xyes" -a "x$with_x" = "xno" ; then
     no)                AC_MSG_RESULT(no GUI support)
                SKIP_PHOTON=YES ;;
     yes|""|auto) AC_MSG_RESULT(automatic GUI support)
-               gui_auto=yes ;;
+               gui_auto=yes ;;
     photon)    AC_MSG_RESULT(Photon GUI support) ;;
     *)         AC_MSG_RESULT([Sorry, $enable_gui GUI is not supported])
                SKIP_PHOTON=YES ;;
@@ -2656,7 +2657,7 @@ elif test "x$MACOS_X" = "xyes" -a "x$with_x" = "xno" ; then
   case "$enable_gui_canon" in
     no)                AC_MSG_RESULT(no GUI support) ;;
     yes|"")    AC_MSG_RESULT(yes - automatic GUI support)
-               gui_auto=yes ;;
+               gui_auto=yes ;;
     auto)      AC_MSG_RESULT(auto - disable GUI support for Mac OS) ;;
     *)         AC_MSG_RESULT([Sorry, $enable_gui GUI is not supported]) ;;
   esac
@@ -2668,6 +2669,7 @@ else
                gui_auto=yes
                SKIP_GTK2=
                SKIP_GTK3=
+               SKIP_GTK4=
                SKIP_GNOME=
                SKIP_MOTIF=;;
     gtk2)      AC_MSG_RESULT(GTK+ 2.x GUI support)
@@ -2677,6 +2679,8 @@ else
                SKIP_GTK2=;;
     gtk3)      AC_MSG_RESULT(GTK+ 3.x GUI support)
                SKIP_GTK3=;;
+    gtk4)      AC_MSG_RESULT(GTK 4.x GUI support)
+               SKIP_GTK4=;;
     motif)     AC_MSG_RESULT(Motif GUI support)
                SKIP_MOTIF=;;
     *)         AC_MSG_RESULT([Sorry, $enable_gui GUI is not supported]) ;;
@@ -2708,6 +2712,17 @@ if test "x$SKIP_GNOME" != "xYES" -a "$enable_gui_canon" != "gnome2"; then
   fi
 fi
 
+if test "x$SKIP_GTK4" != "xYES" -a "$enable_gui_canon" != "gtk4"; then
+  AC_MSG_CHECKING(whether or not to look for GTK 4)
+  AC_ARG_ENABLE(gtk4-check,
+       [  --enable-gtk4-check     If auto-select GUI, check for GTK 4 [default=yes]],
+       , enable_gtk4_check="yes")
+  AC_MSG_RESULT($enable_gtk4_check)
+  if test "x$enable_gtk4_check" = "xno"; then
+    SKIP_GTK4=YES
+  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,
@@ -2747,6 +2762,7 @@ AC_DEFUN(AM_PATH_GTK,
     AS_CASE([$min_gtk_version],
            [2.*], [gtk_pkg_name="gtk+-2.0"],
            [3.*], [gtk_pkg_name="gtk+-3.0"],
+           [4.*], [gtk_pkg_name="gtk4"],
                   [AC_MSG_FAILURE([The configure script does not know which pkg-config name to use for GTK $min_gtk_version"])])
 
     AC_MSG_CHECKING([for pkg-config $gtk_pkg_name])
@@ -2754,9 +2770,9 @@ AC_DEFUN(AM_PATH_GTK,
          [
             AC_MSG_RESULT(found)
             AC_MSG_CHECKING([for GTK - version >= $min_gtk_version])
-            dnl We should be using PKG_CHECK_MODULES() instead of this hack.
-            dnl But I guess the dependency on pkgconfig.m4 is not wanted or
-            dnl something like that.
+dnl We should be using PKG_CHECK_MODULES() instead of this hack.
+dnl But I guess the dependency on pkgconfig.m4 is not wanted or
+dnl something like that.
             GTK_CPPFLAGS=`$PKG_CONFIG --cflags-only-I $gtk_pkg_name`
             GTK_CFLAGS=`$PKG_CONFIG --cflags-only-other $gtk_pkg_name`
             GTK_LIBDIR=`$PKG_CONFIG --libs-only-L $gtk_pkg_name`
@@ -2935,7 +2951,7 @@ AC_DEFUN([GNOME_INIT],[
 ])
 
 dnl ---------------------------------------------------------------------------
-dnl Check for GTK3. If it succeeds, skip the check for GTK2.
+dnl Check for GTK3. If it succeeds, skip the check for GTK2/GTK4
 dnl ---------------------------------------------------------------------------
 if test -z "$SKIP_GTK3"; then
   AC_MSG_CHECKING(--disable-gtktest argument)
@@ -2954,6 +2970,7 @@ if test -z "$SKIP_GTK3"; then
                 GUI_INC_LOC="$GTK_CPPFLAGS"])
     if test -n "$GTK_CPPFLAGS"; then
       SKIP_GTK2=YES
+      SKIP_GTK4=YES
       SKIP_GNOME=YES
       SKIP_MOTIF=YES
       GUITYPE=GTK
@@ -2963,6 +2980,47 @@ if test -z "$SKIP_GTK3"; then
   fi
 fi
 
+dnl ---------------------------------------------------------------------------
+dnl Check for GTK4. If it succeeds, skip the check for GTK3/GTK2.
+dnl ---------------------------------------------------------------------------
+if test -z "$SKIP_GTK4"; then
+  AC_MSG_CHECKING(--disable-gtktest argument)
+  AC_ARG_ENABLE(gtktest, [  --disable-gtktest       Do not try to compile and run a test GTK program],
+       , enable_gtktest=yes)
+  if test "x$enable_gtktest" = "xyes" ; then
+    AC_MSG_RESULT(gtk test enabled)
+  else
+    AC_MSG_RESULT(gtk test disabled)
+  fi
+
+  if test "x$PKG_CONFIG" != "xno"; then
+    AM_PATH_GTK(4.0.0,
+               [GUI_LIB_LOC="$GTK_LIBDIR"
+                GTK_LIBNAME="$GTK_LIBS"
+                GUI_INC_LOC="$GTK_CPPFLAGS"])
+    if test -n "$GTK_CPPFLAGS"; then
+      SKIP_GTK3=YES
+      SKIP_GTK2=YES
+      SKIP_GNOME=YES
+      SKIP_MOTIF=YES
+      GUITYPE=GTK4
+      AC_SUBST(GTK_LIBNAME)
+      AC_DEFINE(USE_GTK4)
+      dnl GTK4 does not use any X11 APIs directly.
+      dnl GTK4 itself links against X11 for its backend, so the
+      dnl dynamic linker resolves X11 symbols via GTK4's dependency.
+      X_LIBS=
+      X_PRE_LIBS=
+      X_EXTRA_LIBS=
+      X_LIB=
+      dnl automatically enable XIM for GTK4
+      if test "$enable_xim" = "auto"; then
+        enable_xim="yes"
+      fi
+    fi
+  fi
+fi
+
 dnl ---------------------------------------------------------------------------
 dnl Check for GTK2.  If it fails, then continue on for Motif as before...
 dnl ---------------------------------------------------------------------------
index ed03592635511f7363ea9e8819e7fc9e0dea8126..01c19298b96272b71f3e8527d5e290b7a964a7c4 100644 (file)
@@ -7133,6 +7133,13 @@ f_has(typval_T *argvars, typval_T *rettv)
                1
 #else
                0
+#endif
+               },
+       {"gui_gtk4",
+#if defined(FEAT_GUI_GTK) && defined(USE_GTK4)
+               1
+#else
+               0
 #endif
                },
        {"gui_gnome",
index 3f3c67a17673ecb8bfecf7e2151125aca93e5447..115db703d2d59eed63b18d1861f5487cbc41f2c2 100644 (file)
 # define FEAT_POSTSCRIPT
 #endif
 
+/*
+ * +gtk_print          Native GTK print dialog for :hardcopy (GTK4).
+ *                     Uses GtkPrintOperation + Pango/Cairo instead of PostScript.
+ */
+#if defined(FEAT_PRINTER) && defined(FEAT_GUI_GTK) && defined(USE_GTK4)
+# define FEAT_GUI_GTK_PRINT
+#endif
+
 /*
  * +diff               Displaying diffs in a nice way.
  *                     Can be enabled in autoconf already.
 /*
  * GUI dark theme variant
  */
-#if (defined(FEAT_GUI_GTK) && defined(USE_GTK3)) || defined(FEAT_GUI_MSWIN)
+#if (defined(FEAT_GUI_GTK) && (defined(USE_GTK3) || defined(USE_GTK4))) \
+       || defined(FEAT_GUI_MSWIN)
 # define FEAT_GUI_DARKTHEME
 #endif
 
  * +X11                        Unix only.  Include code for xterm title saving and X
  *                     clipboard.  Only works if HAVE_X11 is also defined.
  */
-#if defined(FEAT_NORMAL) || defined(FEAT_GUI_MOTIF)
+#if (defined(FEAT_NORMAL) || defined(FEAT_GUI_MOTIF)) && !defined(USE_GTK4)
 # define WANT_X11
 #endif
 
 
 #if defined(FEAT_NORMAL) \
        && (defined(UNIX) || defined(VMS)) \
-       && defined(WANT_X11) && defined(HAVE_X11)
+       && defined(WANT_X11) && defined(HAVE_X11) \
+       && !defined(USE_GTK4)
 # define FEAT_XCLIPBOARD
 # ifndef FEAT_CLIPBOARD
 #  define FEAT_CLIPBOARD
index 2e3079a1e499f038f5292ffb7af6bb89c577aa0e..b995698128eb579ffd83571a1708b258d0a55617 100644 (file)
@@ -2018,7 +2018,7 @@ vgetc(void)
                    continue;
                }
 #endif
-#if defined(FEAT_GUI) && defined(FEAT_GUI_GTK) && defined(FEAT_MENU)
+#if defined(FEAT_GUI) && defined(FEAT_GUI_GTK) && !defined(USE_GTK4) && defined(FEAT_MENU)
                // GTK: <F10> normally selects the menu, but it's passed until
                // here to allow mapping it.  Intercept and invoke the GTK
                // behavior if it's not mapped.
index bf277e2b93baefee857b5a52d308cf675b6b9481..1448c86bdb71bc7e3deec82551139bb17533b374 100644 (file)
--- a/src/gui.c
+++ b/src/gui.c
@@ -1709,6 +1709,12 @@ gui_set_shellsize(
 #if defined(MSWIN) || defined(FEAT_GUI_GTK)
     // If not setting to a user specified size and maximized, calculate the
     // number of characters that fit in the maximized window.
+    // FIXME: gui_mch_newfont() is called here even when the font hasn't
+    // changed at all.  For example, ":set guioptions=k" triggers this path
+    // via gui_init_which_components() -> gui_set_shellsize(FALSE, ...).
+    // The intent is to keep the window size and recalculate Rows/Columns,
+    // which has nothing to do with fonts.  This should be a separate
+    // function with a more descriptive name.
     if (!mustset && (vim_strchr(p_go, GO_KEEPWINSIZE) != NULL
                                                       || gui_mch_maximized()))
     {
index 4639b3a1294e53aa9499e588919f6d9f633e587a..d1e5d2d5c062f21b07fa555b0a7cb1e855913559 100644 (file)
--- a/src/gui.h
+++ b/src/gui.h
 # ifdef VMS
 #  include "gui_gtk_vms.h"
 # endif
-# include <X11/Intrinsic.h>
+# ifdef USE_GTK4
+// Types used in proto files but not available without X11 headers
+typedef void *Widget;
+typedef void *XtAppContext;
+typedef void  Display;
+typedef unsigned long Window;
+typedef unsigned long Atom;
+typedef GdkEvent GdkEventKey;  // GTK4: GdkEventKey merged into GdkEvent
+# else
+#  include <X11/Intrinsic.h>
+# endif
 # pragma GCC diagnostic push
 # pragma GCC diagnostic ignored "-Wstrict-prototypes"
 # include <gtk/gtk.h>
@@ -344,7 +354,7 @@ typedef struct Gui
 #endif
 
 #ifdef FEAT_GUI_GTK
-# ifndef USE_GTK3
+# if !defined(USE_GTK3) && !defined(USE_GTK4)
     int                visibility;         // Is shell partially/fully obscured?
 # endif
     GdkCursor  *blank_pointer;     // Blank pointer
@@ -365,7 +375,7 @@ typedef struct Gui
     GtkWidget  *menubar_h;         // menubar handle
     GtkWidget  *toolbar_h;         // toolbar handle
 # endif
-# ifdef USE_GTK3
+# if defined(USE_GTK3) || defined(USE_GTK4)
     GdkRGBA    *fgcolor;           // GDK-styled foreground color
     GdkRGBA    *bgcolor;           // GDK-styled background color
     GdkRGBA    *spcolor;           // GDK-styled special color
@@ -374,7 +384,7 @@ typedef struct Gui
     GdkColor   *bgcolor;           // GDK-styled background color
     GdkColor   *spcolor;           // GDK-styled special color
 # endif
-# ifdef USE_GTK3
+# if defined(USE_GTK3) || defined(USE_GTK4)
     cairo_surface_t *surface;       // drawarea surface
 # else
     GdkGC      *text_gc;           // cached GC for normal text
@@ -386,7 +396,9 @@ typedef struct Gui
     GtkWidget  *tabline;           // tab pages line handle
 # endif
 
+# ifndef USE_GTK4
     GtkAccelGroup *accel_group;
+# endif
     GtkWidget  *filedlg;           // file selection dialog
     char_u     *browse_fname;      // file name from filedlg
 
@@ -553,7 +565,7 @@ typedef enum
  * For Solaris Studio, that is not the case.  An explicit type cast is needed
  * to suppress warnings on that particular conversion.
  */
-# if defined(__SUNPRO_C) && defined(USE_GTK3)
+# if defined(__SUNPRO_C) && (defined(USE_GTK3) || defined(USE_GTK4))
 #  define FUNC2GENERIC(func) (void *)(func)
 # else
 #  define FUNC2GENERIC(func) G_CALLBACK(func)
index 1d41089fec05fc18a72315564fbcd30ea5c1e9d1..c928d25685b3e573f42c2c94de8b87dc1e8967f3 100644 (file)
@@ -16,7 +16,9 @@
 #if !defined(FEAT_GUI_MSWIN)
 
 # ifdef FEAT_GUI_GTK
-#  if GTK_CHECK_VERSION(3,0,0)
+#  ifdef USE_GTK4
+#   include <gdk/gdkkeysyms.h>
+#  elif GTK_CHECK_VERSION(3,0,0)
 #   include <gdk/gdkkeysyms-compat.h>
 #  else
 #   include <gdk/gdkkeysyms.h>
@@ -390,7 +392,11 @@ pointer_event(BalloonEval *beval, int x, int y, unsigned state)
        beval->x = x;
        beval->y = y;
 
+#  ifdef USE_GTK4
+       if (state & (int)GDK_ALT_MASK)
+#  else
        if (state & (int)GDK_MOD1_MASK)
+#  endif
        {
            /*
             * Alt is pressed -- enter super-evaluate-mode,
@@ -416,14 +422,24 @@ key_event(BalloonEval *beval, unsigned keyval, int is_keypress)
     {
        switch (keyval)
        {
+#  ifdef USE_GTK4
+           case GDK_KEY_Shift_L:
+           case GDK_KEY_Shift_R:
+#  else
            case GDK_Shift_L:
            case GDK_Shift_R:
+#  endif
                beval->showState = ShS_UPDATE_PENDING;
                (*beval->msgCB)(beval, (is_keypress)
                                                   ? (int)GDK_SHIFT_MASK : 0);
                break;
+#  ifdef USE_GTK4
+           case GDK_KEY_Control_L:
+           case GDK_KEY_Control_R:
+#  else
            case GDK_Control_L:
            case GDK_Control_R:
+#  endif
                beval->showState = ShS_UPDATE_PENDING;
                (*beval->msgCB)(beval, (is_keypress)
                                                 ? (int)GDK_CONTROL_MASK : 0);
diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c
new file mode 100644 (file)
index 0000000..24ad6dd
--- /dev/null
@@ -0,0 +1,4638 @@
+/* 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.
+ *
+ * GTK4 GUI implementation: main window, events, drawing, menus,
+ * scrollbars, dialogs, and toolbar.  This is a clean implementation for
+ * GTK4, separate from gui_gtk_x11.c which handles GTK2/GTK3.
+ *
+ * GTK4 differences from GTK3:
+ * - No GdkWindow (use GdkSurface for top-level only)
+ * - No GtkContainer (use gtk_widget_set_parent/gtk_box_append)
+ * - Events via GtkEventController, not signal+mask
+ * - Drawing via GtkSnapshot or gtk_drawing_area_set_draw_func
+ * - No gtk_dialog_run (async dialogs)
+ * - No GdkAtom (string-based content types)
+ * - No GtkSocket/GtkPlug
+ * - gtk_window_new() takes no arguments
+ */
+
+#include "vim.h"
+
+#ifdef FEAT_GUI_GTK
+
+#include <gdk/gdkkeysyms.h>
+#include <gdk/gdk.h>
+#include <gtk/gtk.h>
+#include "gui_gtk4_f.h"
+
+/*
+ * Geometry string parser, replacing XParseGeometry to remove X11 dependency.
+ * Format: [WIDTHxHEIGHT][{+-}XOFF{+-}YOFF]
+ */
+#define NoValue                0x0000
+#define XValue         0x0001
+#define YValue         0x0002
+#define WidthValue     0x0004
+#define HeightValue    0x0008
+#define XNegative      0x0010
+#define YNegative      0x0020
+
+    static int
+vim_parse_geometry(const char *str, int *x, int *y,
+       unsigned int *width, unsigned int *height)
+{
+    int mask = NoValue;
+    char *end;
+    long val;
+
+    if (str == NULL || *str == NUL)
+       return mask;
+
+    // Parse width
+    if (*str != '+' && *str != '-')
+    {
+       val = strtol(str, &end, 10);
+       if (end != str)
+       {
+           *width = (unsigned int)val;
+           mask |= WidthValue;
+           str = end;
+       }
+    }
+
+    // Parse 'x' or 'X' separator and height
+    if (*str == 'x' || *str == 'X')
+    {
+       str++;
+       val = strtol(str, &end, 10);
+       if (end != str)
+       {
+           *height = (unsigned int)val;
+           mask |= HeightValue;
+           str = end;
+       }
+    }
+
+    // Parse x offset
+    if (*str == '+' || *str == '-')
+    {
+       int negative = (*str == '-');
+       str++;
+       val = strtol(str, &end, 10);
+       if (end != str)
+       {
+           *x = negative ? -(int)val : (int)val;
+           mask |= XValue;
+           if (negative)
+               mask |= XNegative;
+           str = end;
+       }
+    }
+
+    // Parse y offset
+    if (*str == '+' || *str == '-')
+    {
+       int negative = (*str == '-');
+       str++;
+       val = strtol(str, &end, 10);
+       if (end != str)
+       {
+           *y = negative ? -(int)val : (int)val;
+           mask |= YValue;
+           if (negative)
+               mask |= YNegative;
+       }
+    }
+
+    return mask;
+}
+
+#ifdef FEAT_SOCKETSERVER
+# include <glib-unix.h>
+
+// Used to track the source for the listening socket
+static guint socket_server_source_id = 0;
+#endif
+
+#if defined(FEAT_MOUSESHAPE)
+// Last set mouse pointer shape
+static int last_shape = 0;
+#endif
+
+#define DEFAULT_FONT   "Monospace 10"
+
+// Menu action group for GMenu-based menus
+static GSimpleActionGroup *menu_action_group = NULL;
+
+// Cursor blinking state
+static enum {
+    BLINK_NONE,
+    BLINK_OFF,
+    BLINK_ON
+} blink_state = BLINK_NONE;
+
+// GTK4 main loop compatibility
+static int gtk4_main_loop_level = 0;
+static int gtk4_main_loop_quit = FALSE;
+
+#ifdef USE_GRESOURCE
+# include "auto/gui_gtk_gresources.h"
+#endif
+
+typedef gboolean timeout_cb_type;
+
+/*
+ * Table of special key mappings.
+ */
+static struct special_key
+{
+    guint key_sym;
+    char_u code0;
+    char_u code1;
+}
+const special_keys[] =
+{
+    {GDK_KEY_Up,       'k', 'u'},
+    {GDK_KEY_Down,     'k', 'd'},
+    {GDK_KEY_Left,     'k', 'l'},
+    {GDK_KEY_Right,    'k', 'r'},
+    {GDK_KEY_F1,       'k', '1'},
+    {GDK_KEY_F2,       'k', '2'},
+    {GDK_KEY_F3,       'k', '3'},
+    {GDK_KEY_F4,       'k', '4'},
+    {GDK_KEY_F5,       'k', '5'},
+    {GDK_KEY_F6,       'k', '6'},
+    {GDK_KEY_F7,       'k', '7'},
+    {GDK_KEY_F8,       'k', '8'},
+    {GDK_KEY_F9,       'k', '9'},
+    {GDK_KEY_F10,      'k', ';'},
+    {GDK_KEY_F11,      'F', '1'},
+    {GDK_KEY_F12,      'F', '2'},
+    {GDK_KEY_Help,     '%', '1'},
+    {GDK_KEY_Undo,     '&', '8'},
+    {GDK_KEY_BackSpace,        'k', 'b'},
+    {GDK_KEY_Insert,   'k', 'I'},
+    {GDK_KEY_Delete,   'k', 'D'},
+    {GDK_KEY_Home,     'k', 'h'},
+    {GDK_KEY_End,      '@', '7'},
+    {GDK_KEY_Prior,    'k', 'P'},
+    {GDK_KEY_Next,     'k', 'N'},
+    {GDK_KEY_Print,    '%', '9'},
+    {GDK_KEY_KP_Left,  'k', 'l'},
+    {GDK_KEY_KP_Right, 'k', 'r'},
+    {GDK_KEY_KP_Up,    'k', 'u'},
+    {GDK_KEY_KP_Down,  'k', 'd'},
+    {GDK_KEY_KP_Insert,        KS_EXTRA, (char_u)KE_KINS},
+    {GDK_KEY_KP_Delete,        KS_EXTRA, (char_u)KE_KDEL},
+    {GDK_KEY_KP_Home,  'K', '1'},
+    {GDK_KEY_KP_End,   'K', '4'},
+    {GDK_KEY_KP_Prior, 'K', '3'},
+    {GDK_KEY_KP_Next,  'K', '5'},
+    {GDK_KEY_KP_Add,   'K', '6'},
+    {GDK_KEY_KP_Subtract, 'K', '7'},
+    {GDK_KEY_KP_Divide,        'K', '8'},
+    {GDK_KEY_KP_Multiply, 'K', '9'},
+    {GDK_KEY_KP_Enter, 'K', 'A'},
+    {GDK_KEY_KP_Decimal,       'K', 'B'},
+    {GDK_KEY_KP_0,     'K', 'C'},
+    {GDK_KEY_KP_1,     'K', 'D'},
+    {GDK_KEY_KP_2,     'K', 'E'},
+    {GDK_KEY_KP_3,     'K', 'F'},
+    {GDK_KEY_KP_4,     'K', 'G'},
+    {GDK_KEY_KP_5,     'K', 'H'},
+    {GDK_KEY_KP_6,     'K', 'I'},
+    {GDK_KEY_KP_7,     'K', 'J'},
+    {GDK_KEY_KP_8,     'K', 'K'},
+    {GDK_KEY_KP_9,     'K', 'L'},
+    {0, 0, 0}
+};
+
+    static int
+keyval_to_string(unsigned int keyval, char_u *string)
+{
+    int                len;
+    guint32    uc;
+
+    uc = gdk_keyval_to_unicode(keyval);
+    if (uc != 0)
+    {
+       len = utf_char2bytes((int)uc, string);
+    }
+    else
+    {
+       len = 1;
+       switch (keyval)
+       {
+           case GDK_KEY_Tab: case GDK_KEY_KP_Tab: case GDK_KEY_ISO_Left_Tab:
+               string[0] = TAB;
+               break;
+           case GDK_KEY_Linefeed:
+               string[0] = NL;
+               break;
+           case GDK_KEY_Return: case GDK_KEY_ISO_Enter: case GDK_KEY_3270_Enter:
+               string[0] = CAR;
+               break;
+           case GDK_KEY_Escape:
+               string[0] = ESC;
+               break;
+           default:
+               len = 0;
+               break;
+       }
+    }
+    string[len] = NUL;
+    return len;
+}
+
+    static int
+modifiers_gdk2vim(guint state)
+{
+    int modifiers = 0;
+
+    if (state & GDK_SHIFT_MASK)
+       modifiers |= MOD_MASK_SHIFT;
+    if (state & GDK_CONTROL_MASK)
+       modifiers |= MOD_MASK_CTRL;
+    if (state & GDK_ALT_MASK)
+       modifiers |= MOD_MASK_ALT;
+    if (state & GDK_META_MASK)
+       modifiers |= MOD_MASK_META;
+    if (state & GDK_SUPER_MASK)
+       modifiers |= MOD_MASK_CMD;
+
+    return modifiers;
+}
+
+static GtkWidget *vbox;                // the main vertical box
+
+// Forward declarations for event callbacks
+static void draw_event(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data);
+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);
+static void button_release_event(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data);
+static void motion_notify_event(GtkEventControllerMotion *controller, double x, double y, gpointer data);
+static void enter_notify_event(GtkEventControllerMotion *controller, double x, double y, gpointer data);
+static void leave_notify_event(GtkEventControllerMotion *controller, gpointer data);
+static gboolean scroll_event(GtkEventControllerScroll *controller, double dx, double dy, gpointer data);
+static void focus_in_event(GtkEventControllerFocus *controller, gpointer data);
+static void focus_out_event(GtkEventControllerFocus *controller, gpointer data);
+#ifdef FEAT_DND
+static gboolean drop_cb(GtkDropTarget *target, const GValue *value, double x, double y, gpointer data);
+#endif
+static void mainwin_destroy_cb(GObject *object, gpointer data);
+static gboolean delete_event_cb(GtkWindow *window, gpointer data);
+static void drawarea_realize_cb(GtkWidget *widget, gpointer data);
+static void drawarea_unrealize_cb(GtkWidget *widget, gpointer data);
+static void drawarea_resize_cb(GtkDrawingArea *area, int width, int height, gpointer data);
+
+/*
+ * Parse the GUI related command-line arguments.  Any arguments used are
+ * deleted from argv, and *argc is decremented accordingly.  This is called
+ * when vim is started, whether or not the GUI has been started.
+ */
+    void
+gui_mch_prepare(int *argc, char **argv)
+{
+    // Don't call gtk_init() here.  It will be called in
+    // gui_mch_init_check() after the fork.  Calling it before fork
+    // breaks the display connection in the child process, causing gvim
+    // to fail to start without --nofork.
+}
+
+/*
+ * Free all GUI related resources.
+ */
+    void
+gui_mch_free_all(void)
+{
+}
+
+    static guint
+timeout_add(int time, timeout_cb_type (*callback)(gpointer), int *flagp)
+{
+    return g_timeout_add((guint)time, (GSourceFunc)callback, flagp);
+}
+
+    static void
+timeout_remove(guint timer)
+{
+    g_source_remove(timer);
+}
+
+static long_u blink_waittime = 700;
+static long_u blink_ontime = 400;
+static long_u blink_offtime = 250;
+static guint blink_timer = 0;
+
+    static timeout_cb_type
+blink_cb(gpointer data UNUSED)
+{
+    if (blink_state == BLINK_ON)
+    {
+       gui_undraw_cursor();
+       blink_state = BLINK_OFF;
+       blink_timer = timeout_add(blink_offtime, blink_cb, NULL);
+    }
+    else
+    {
+       gui_update_cursor(TRUE, FALSE);
+       blink_state = BLINK_ON;
+       blink_timer = timeout_add(blink_ontime, blink_cb, NULL);
+    }
+    return FALSE;
+}
+
+    int
+gui_mch_is_blinking(void)
+{
+    return blink_state != BLINK_NONE;
+}
+
+    int
+gui_mch_is_blink_off(void)
+{
+    return blink_state == BLINK_OFF;
+}
+
+    void
+gui_mch_set_blinking(long waittime, long on, long off)
+{
+    blink_waittime = waittime;
+    blink_ontime = on;
+    blink_offtime = off;
+}
+
+    void
+gui_mch_stop_blink(int may_call_gui_update_cursor)
+{
+    if (blink_timer)
+    {
+       timeout_remove(blink_timer);
+       blink_timer = 0;
+    }
+    if (blink_state == BLINK_OFF && may_call_gui_update_cursor)
+       gui_update_cursor(TRUE, FALSE);
+    blink_state = BLINK_NONE;
+}
+
+    void
+gui_mch_start_blink(void)
+{
+    if (blink_timer)
+    {
+       timeout_remove(blink_timer);
+       blink_timer = 0;
+    }
+    if (blink_waittime && blink_ontime && blink_offtime && gui.in_focus)
+    {
+       blink_timer = timeout_add(blink_waittime, blink_cb, NULL);
+       blink_state = BLINK_ON;
+       gui_update_cursor(TRUE, FALSE);
+    }
+}
+
+    int
+gui_mch_early_init_check(int give_message UNUSED)
+{
+    return OK;
+}
+
+    int
+gui_mch_init_check(void)
+{
+    // This defaults to argv[0], but we want it to match the name of the
+    // shipped gvim.desktop so that Vim's windows can be associated with this
+    // file.  Also sets WM_CLASS on X11.
+    g_set_prgname("gvim");
+
+    // Suppress noisy EGL warnings when GL is not available.  Only set
+    // this when actually starting the GUI, so non-GUI invocations are
+    // not affected.
+    if (g_getenv("EGL_LOG_LEVEL") == NULL)
+       setenv("EGL_LOG_LEVEL", "fatal", 0);
+
+    // Call gtk_init() here after fork().  Calling it before fork() breaks
+    // the display connection in the child process.
+    gtk_init();
+    return OK;
+}
+
+/*
+ * Initialise the GUI.  Create all the windows, set up all the callbacks etc.
+ * Returns OK for success, FAIL when the GUI can't be started.
+ */
+    int
+gui_mch_init(void)
+{
+    // Allocate GdkRGBA color structs.
+    gui.fgcolor = g_new(GdkRGBA, 1);
+    gui.bgcolor = g_new(GdkRGBA, 1);
+    gui.spcolor = g_new(GdkRGBA, 1);
+
+    gui.def_norm_pixel = 0x00000000;   // black
+    gui.def_back_pixel = 0x00ffffff;   // white
+    gui.norm_pixel = gui.def_norm_pixel;
+    gui.back_pixel = gui.def_back_pixel;
+
+    gui.scrollbar_width = SB_DEFAULT_WIDTH;
+    gui.scrollbar_height = SB_DEFAULT_WIDTH;
+
+    // Create the main window.
+    gui.mainwin = gtk_window_new();
+    gtk_widget_set_name(gui.mainwin, "vim-main-window");
+
+    // Create the PangoContext used for drawing all text.
+    gui.text_context = gtk_widget_create_pango_context(gui.mainwin);
+    pango_context_set_base_dir(gui.text_context, PANGO_DIRECTION_LTR);
+
+    g_signal_connect(G_OBJECT(gui.mainwin), "close-request",
+                    G_CALLBACK(delete_event_cb), NULL);
+
+    // A vertical box holds the menubar, toolbar and main text window.
+    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+    gtk_box_set_homogeneous(GTK_BOX(vbox), FALSE);
+    gtk_window_set_child(GTK_WINDOW(gui.mainwin), vbox);
+
+#ifdef FEAT_MENU
+    {
+       GMenu *gmenu = g_menu_new();
+       gui.menubar = gtk_popover_menu_bar_new_from_model(
+               G_MENU_MODEL(gmenu));
+       g_object_set_data_full(G_OBJECT(gui.menubar), "vim-gmenu",
+               gmenu, g_object_unref);
+       gtk_widget_set_name(gui.menubar, "vim-menubar");
+       gtk_widget_set_visible(gui.menubar, FALSE);
+       gtk_box_append(GTK_BOX(vbox), gui.menubar);
+    }
+#endif
+
+#ifdef FEAT_TOOLBAR
+    gui.toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+    gtk_widget_set_name(gui.toolbar, "vim-toolbar");
+    gtk_widget_set_visible(gui.toolbar, FALSE);
+    gtk_box_append(GTK_BOX(vbox), gui.toolbar);
+#endif
+
+#ifdef FEAT_GUI_TABLINE
+    gui.tabline = gtk_notebook_new();
+    gtk_notebook_set_show_border(GTK_NOTEBOOK(gui.tabline), FALSE);
+    gtk_notebook_set_show_tabs(GTK_NOTEBOOK(gui.tabline), FALSE);
+    gtk_notebook_set_scrollable(GTK_NOTEBOOK(gui.tabline), TRUE);
+    gtk_widget_set_visible(gui.tabline, FALSE);
+    gtk_box_append(GTK_BOX(vbox), gui.tabline);
+#endif
+
+    // The form widget manages absolute positioning of scrollbars.
+    gui.formwin = gui_gtk_form_new();
+    gtk_widget_set_name(gui.formwin, "vim-gtk-form");
+    // formwin is overlaid on top of drawarea for scrollbar positioning.
+    // Disable input targeting so mouse events pass through to drawarea.
+    gtk_widget_set_can_target(gui.formwin, FALSE);
+
+    // The drawing area for the editor content.
+    // Placed in an overlay so it fills the formwin, with scrollbars on top.
+    gui.drawarea = gtk_drawing_area_new();
+    gui.surface = NULL;
+    gtk_widget_set_focusable(gui.drawarea, TRUE);
+    gtk_widget_set_vexpand(gui.drawarea, TRUE);
+    gtk_widget_set_hexpand(gui.drawarea, TRUE);
+
+    {
+       // Use GtkOverlay: drawarea as the main child, formwin as overlay
+       GtkWidget *overlay = gtk_overlay_new();
+       gtk_overlay_set_child(GTK_OVERLAY(overlay), gui.drawarea);
+       gtk_overlay_add_overlay(GTK_OVERLAY(overlay), gui.formwin);
+       gtk_widget_set_vexpand(overlay, TRUE);
+       gtk_widget_set_hexpand(overlay, TRUE);
+       gtk_box_append(GTK_BOX(vbox), overlay);
+    }
+
+    // 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);
+
+    // Set up event controllers.
+    {
+       GtkEventController *key_ctrl = gtk_event_controller_key_new();
+       g_signal_connect(key_ctrl, "key-pressed",
+                        G_CALLBACK(key_press_event), NULL);
+       g_signal_connect(key_ctrl, "key-released",
+                        G_CALLBACK(key_release_event), NULL);
+       gtk_widget_add_controller(gui.mainwin, key_ctrl);
+    }
+
+    {
+       GtkGesture *click = gtk_gesture_click_new();
+       gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click), 0);
+       g_signal_connect(click, "pressed",
+                        G_CALLBACK(button_press_event), NULL);
+       g_signal_connect(click, "released",
+                        G_CALLBACK(button_release_event), NULL);
+       gtk_widget_add_controller(gui.drawarea, GTK_EVENT_CONTROLLER(click));
+    }
+
+    {
+       GtkEventController *motion = gtk_event_controller_motion_new();
+       g_signal_connect(motion, "motion",
+                        G_CALLBACK(motion_notify_event), NULL);
+       g_signal_connect(motion, "enter",
+                        G_CALLBACK(enter_notify_event), NULL);
+       g_signal_connect(motion, "leave",
+                        G_CALLBACK(leave_notify_event), NULL);
+       gtk_widget_add_controller(gui.drawarea, motion);
+    }
+
+    {
+       GtkEventController *scroll = gtk_event_controller_scroll_new(
+               GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES);
+       g_signal_connect(scroll, "scroll",
+                        G_CALLBACK(scroll_event), NULL);
+       gtk_widget_add_controller(gui.drawarea, scroll);
+    }
+
+    {
+       GtkEventController *focus = gtk_event_controller_focus_new();
+       g_signal_connect(focus, "enter",
+                        G_CALLBACK(focus_in_event), NULL);
+       g_signal_connect(focus, "leave",
+                        G_CALLBACK(focus_out_event), NULL);
+       gtk_widget_add_controller(gui.drawarea, focus);
+    }
+
+#ifdef FEAT_DND
+    // Set up drag-and-drop target for files and text.
+    {
+       GtkDropTarget *drop = gtk_drop_target_new(G_TYPE_INVALID, GDK_ACTION_COPY);
+       GType types[] = { GDK_TYPE_FILE_LIST, G_TYPE_STRING };
+       gtk_drop_target_set_gtypes(drop, types, 2);
+       g_signal_connect(drop, "drop",
+                        G_CALLBACK(drop_cb), NULL);
+       gtk_widget_add_controller(gui.drawarea, GTK_EVENT_CONTROLLER(drop));
+    }
+#endif
+
+    gui.border_offset = gui.border_width;
+
+    // Create a blank (invisible) cursor for hiding the mouse pointer.
+    gui.blank_pointer = gdk_cursor_new_from_name("none", NULL);
+
+    return OK;
+}
+
+/*
+ * Called when the foreground or background color has been changed.
+ */
+    static void
+surface_fill_bg(void)
+{
+    if (gui.surface != NULL)
+    {
+       cairo_t *cr = cairo_create(gui.surface);
+       cairo_set_source_rgba(cr,
+               gui.bgcolor->red, gui.bgcolor->green,
+               gui.bgcolor->blue, gui.bgcolor->alpha);
+       cairo_paint(cr);
+       cairo_destroy(cr);
+    }
+}
+
+    void
+gui_mch_new_colors(void)
+{
+    surface_fill_bg();
+    if (gui.drawarea != NULL && gtk_widget_get_realized(gui.drawarea))
+       gtk_widget_queue_draw(gui.drawarea);
+}
+
+/*
+ * Open the GUI window which was created by a call to gui_mch_init().
+ */
+    int
+gui_mch_open(void)
+{
+    guicolor_T fg_pixel = INVALCOLOR;
+    guicolor_T bg_pixel = INVALCOLOR;
+    guint pixel_width;
+    guint pixel_height;
+
+    if (gui.geom != NULL)
+    {
+       int             mask;
+       unsigned int    w, h;
+       int             x = 0;
+       int             y = 0;
+
+       mask = vim_parse_geometry((char *)gui.geom, &x, &y, &w, &h);
+
+       if (mask & WidthValue)
+           Columns = w;
+       if (mask & HeightValue)
+       {
+           if (p_window > (long)h - 1 || !option_was_set((char_u *)"window"))
+               p_window = h - 1;
+           Rows = h;
+       }
+       limit_screen_size();
+
+       VIM_CLEAR(gui.geom);
+    }
+
+    // Use 80x24 as the default GUI size, unless geometry was specified.
+    if (Columns > 80 && gui.geom == NULL)
+       Columns = 80;
+    if (Rows > 24 && gui.geom == NULL)
+       Rows = 24;
+    pixel_width = (guint)(gui_get_base_width() + Columns * gui.char_width);
+    pixel_height = (guint)(gui_get_base_height() + Rows * gui.char_height);
+    gtk_window_set_default_size(GTK_WINDOW(gui.mainwin),
+           pixel_width, pixel_height);
+
+    if (foreground_argument != NULL)
+       fg_pixel = gui_get_color((char_u *)foreground_argument);
+    if (fg_pixel == INVALCOLOR)
+       fg_pixel = gui_get_color((char_u *)"Black");
+
+    if (background_argument != NULL)
+       bg_pixel = gui_get_color((char_u *)background_argument);
+    if (bg_pixel == INVALCOLOR)
+       bg_pixel = gui_get_color((char_u *)"White");
+
+    if (found_reverse_arg)
+    {
+       gui.def_norm_pixel = bg_pixel;
+       gui.def_back_pixel = fg_pixel;
+    }
+    else
+    {
+       gui.def_norm_pixel = fg_pixel;
+       gui.def_back_pixel = bg_pixel;
+    }
+
+    set_normal_colors();
+    gui_check_colors();
+    highlight_gui_started();
+
+    g_signal_connect(G_OBJECT(gui.mainwin), "destroy",
+                    G_CALLBACK(mainwin_destroy_cb), NULL);
+    // Resize is handled by GtkForm's size_allocate callback.
+
+    gtk_widget_set_visible(gui.mainwin, TRUE);
+
+    // Make sure the drawing area gets keyboard focus.
+    gtk_widget_grab_focus(gui.drawarea);
+    gui_focus_change(TRUE);
+
+    return OK;
+}
+
+    void
+gui_mch_exit(int rc UNUSED)
+{
+    if (gui.mainwin != NULL)
+       gtk_window_destroy(GTK_WINDOW(gui.mainwin));
+}
+
+    int
+gui_mch_get_winpos(int *x, int *y)
+{
+    // GTK4 does not provide a window position API.
+    *x = 0;
+    *y = 0;
+    return FAIL;
+}
+
+    void
+gui_mch_set_winpos(int x UNUSED, int y UNUSED)
+{
+    // GTK4/Wayland: window positioning not available
+}
+
+    int
+gui_mch_maximized(void)
+{
+    return gtk_window_is_maximized(GTK_WINDOW(gui.mainwin));
+}
+
+    void
+gui_mch_unmaximize(void)
+{
+    if (gui.mainwin != NULL)
+       gtk_window_unmaximize(GTK_WINDOW(gui.mainwin));
+}
+
+/*
+ * Called when the font changed while the window is maximized or GO_KEEPWINSIZE
+ * is set.  Recalculate Rows and Columns based on the current window size.
+ *
+ * NOTE: gui_set_shellsize() calls this when GO_KEEPWINSIZE ('k') is in
+ * 'guioptions', even when the font hasn't actually changed (e.g. just setting
+ * "guioptions=k" triggers it via gui_init_which_components()).  This is
+ * arguably a design problem in the common code, but we must not call
+ * gui_set_shellsize() back from here or it will cause infinite recursion and
+ * crash.  Use gui_resize_shell() to recalculate Rows/Columns from the current
+ * window size instead.
+ */
+    void
+gui_mch_newfont(void)
+{
+    int w, h;
+
+    w = gtk_widget_get_width(gui.formwin);
+    h = gtk_widget_get_height(gui.formwin);
+    w -= get_menu_tool_width();
+    h -= get_menu_tool_height();
+    gui_resize_shell(w, h);
+}
+
+    void
+gui_mch_settitle(char_u *title, char_u *icon UNUSED)
+{
+    if (title != NULL && gui.mainwin != NULL)
+       gtk_window_set_title(GTK_WINDOW(gui.mainwin), (const char *)title);
+}
+
+static int in_set_shellsize = FALSE;
+
+    void
+gui_mch_set_shellsize(int width, int height,
+       int min_width UNUSED, int min_height UNUSED,
+       int base_width UNUSED, int base_height UNUSED,
+       int direction UNUSED)
+{
+    // Only set window size if it hasn't been shown yet (initial sizing).
+    // After that, the window size is controlled by the user/WM and
+    // Vim adapts to it via form_size_allocate -> gui_resize_shell.
+    if (!gtk_widget_get_realized(gui.mainwin))
+    {
+       width += get_menu_tool_width();
+       height += get_menu_tool_height();
+       gtk_window_set_default_size(GTK_WINDOW(gui.mainwin), width, height);
+    }
+}
+
+    void
+gui_mch_get_screen_dimensions(int *screen_w, int *screen_h)
+{
+    GdkDisplay *display = gtk_widget_get_display(gui.mainwin);
+    GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(gui.mainwin));
+
+    if (surface != NULL)
+    {
+       GdkMonitor *monitor = gdk_display_get_monitor_at_surface(display,
+               surface);
+       if (monitor != NULL)
+       {
+           GdkRectangle geom;
+           gdk_monitor_get_geometry(monitor, &geom);
+           *screen_w = geom.width;
+           *screen_h = geom.height;
+           return;
+       }
+    }
+
+    *screen_w = 800;
+    *screen_h = 600;
+}
+
+#ifdef FEAT_MENU
+    void
+gui_mch_enable_menu(int showit)
+{
+    if (gui.menubar != NULL)
+       gtk_widget_set_visible(gui.menubar, showit);
+}
+#endif
+
+#ifdef FEAT_TOOLBAR
+    void
+gui_mch_show_toolbar(int showit)
+{
+    if (gui.toolbar != NULL)
+       gtk_widget_set_visible(gui.toolbar, showit);
+}
+#endif
+
+    void
+gui_mch_set_dark_theme(int dark)
+{
+    // GTK4: use GtkSettings
+    GtkSettings *settings = gtk_settings_get_default();
+    if (settings != NULL)
+       g_object_set(settings, "gtk-application-prefer-dark-theme",
+               (gboolean)dark, NULL);
+}
+
+/*
+ * ============================================================
+ * Font handling
+ * ============================================================
+ */
+
+    int
+gui_mch_adjust_charheight(void)
+{
+    PangoFontMetrics *metrics;
+    int ascent;
+    int descent;
+
+    metrics = pango_context_get_metrics(gui.text_context, gui.norm_font,
+                           pango_context_get_language(gui.text_context));
+    ascent = pango_font_metrics_get_ascent(metrics);
+    descent = pango_font_metrics_get_descent(metrics);
+    pango_font_metrics_unref(metrics);
+
+    gui.char_height = (ascent + descent + (PANGO_SCALE * 15) / 16)
+                                                  / PANGO_SCALE + p_linespace;
+    gui.char_ascent = PANGO_PIXELS(ascent + p_linespace * PANGO_SCALE / 2);
+    gui.char_ascent = MAX(gui.char_ascent, 0);
+    gui.char_height = MAX(gui.char_height, gui.char_ascent + 1);
+
+    return OK;
+}
+
+typedef struct {
+    PangoFontDescription    *result;
+    gboolean               done;
+} FontDialogData;
+
+    static void
+font_dialog_finish_cb(GObject *source, GAsyncResult *res, gpointer data)
+{
+    FontDialogData *fdd = (FontDialogData *)data;
+    fdd->result = gtk_font_dialog_choose_font_finish(
+                   GTK_FONT_DIALOG(source), res, NULL);
+    fdd->done = TRUE;
+}
+
+    static gboolean
+font_filter(gpointer item, gpointer data UNUSED)
+{
+    if (PANGO_IS_FONT_FAMILY(item))
+       return pango_font_family_is_monospace(PANGO_FONT_FAMILY(item));
+    if (PANGO_IS_FONT_FACE(item))
+    {
+       PangoFontFamily *family = pango_font_face_get_family(
+               PANGO_FONT_FACE(item));
+       if (family != NULL)
+           return pango_font_family_is_monospace(family);
+    }
+    return TRUE;
+}
+
+    char_u *
+gui_mch_font_dialog(char_u *oldval)
+{
+    GtkFontDialog      *dlg;
+    PangoFontDescription *initial = NULL;
+    char_u             *fontname = NULL;
+    FontDialogData     fdd;
+
+    dlg = gtk_font_dialog_new();
+    gtk_font_dialog_set_modal(dlg, TRUE);
+    gtk_font_dialog_set_filter(dlg,
+           GTK_FILTER(gtk_custom_filter_new(
+                   (GtkCustomFilterFunc)font_filter, NULL, NULL)));
+
+    if (oldval != NULL && oldval[0] != NUL)
+    {
+       char_u *oldname;
+
+       if (output_conv.vc_type != CONV_NONE)
+           oldname = string_convert(&output_conv, oldval, NULL);
+       else
+           oldname = oldval;
+
+       if (STRLEN(oldname) > 0 && !vim_isdigit(oldname[STRLEN(oldname) - 1]))
+       {
+           char_u *p = vim_strnsave(oldname, STRLEN(oldname) + 3);
+           if (p != NULL)
+           {
+               STRCPY(p + STRLEN(p), " 10");
+               if (oldname != oldval)
+                   vim_free(oldname);
+               oldname = p;
+           }
+       }
+
+       initial = pango_font_description_from_string((const char *)oldname);
+       if (oldname != oldval)
+           vim_free(oldname);
+    }
+    else
+       initial = pango_font_description_from_string(DEFAULT_FONT);
+
+    fdd.result = NULL;
+    fdd.done = FALSE;
+
+    gtk_font_dialog_choose_font(dlg, GTK_WINDOW(gui.mainwin),
+           initial, NULL, font_dialog_finish_cb, &fdd);
+
+    while (!fdd.done)
+       g_main_context_iteration(NULL, TRUE);
+
+    if (fdd.result != NULL)
+    {
+       char *name = pango_font_description_to_string(fdd.result);
+       if (name != NULL)
+       {
+           char_u *p;
+
+           p = vim_strsave_escaped((char_u *)name, (char_u *)",");
+           g_free(name);
+           if (p != NULL && input_conv.vc_type != CONV_NONE)
+           {
+               fontname = string_convert(&input_conv, p, NULL);
+               vim_free(p);
+           }
+           else
+               fontname = p;
+       }
+       pango_font_description_free(fdd.result);
+    }
+
+    if (initial != NULL)
+       pango_font_description_free(initial);
+    g_object_unref(dlg);
+
+    return fontname;
+}
+
+/*
+ * Build a table of glyphs for ASCII characters 32..126.
+ * This avoids the overhead of itemize+shape for the common case.
+ */
+    static void
+ascii_glyph_table_init(void)
+{
+    char_u         ascii_chars[2 * 128];
+    PangoAttrList   *attr_list;
+    GList          *item_list;
+    int                    i;
+
+    if (gui.ascii_glyphs != NULL)
+       pango_glyph_string_free(gui.ascii_glyphs);
+    if (gui.ascii_font != NULL)
+       g_object_unref(gui.ascii_font);
+
+    gui.ascii_glyphs = NULL;
+    gui.ascii_font   = NULL;
+
+    for (i = 0; i < 128; ++i)
+    {
+       if (i >= 32 && i < 127)
+           ascii_chars[2 * i] = i;
+       else
+           ascii_chars[2 * i] = '?';
+       ascii_chars[2 * i + 1] = ' ';
+    }
+
+    attr_list = pango_attr_list_new();
+    item_list = pango_itemize(gui.text_context, (const char *)ascii_chars,
+                             0, sizeof(ascii_chars), attr_list, NULL);
+
+    if (item_list != NULL && item_list->next == NULL)
+    {
+       PangoItem   *item;
+       int         width;
+
+       item  = (PangoItem *)item_list->data;
+       width = gui.char_width * PANGO_SCALE;
+
+       gui.ascii_font = item->analysis.font;
+       g_object_ref(gui.ascii_font);
+
+       gui.ascii_glyphs = pango_glyph_string_new();
+
+       pango_shape((const char *)ascii_chars, sizeof(ascii_chars),
+                   &item->analysis, gui.ascii_glyphs);
+
+       if (gui.ascii_glyphs->num_glyphs == (int)sizeof(ascii_chars))
+       {
+           for (i = 0; i < gui.ascii_glyphs->num_glyphs; ++i)
+           {
+               PangoGlyphGeometry *geom;
+
+               geom = &gui.ascii_glyphs->glyphs[i].geometry;
+               geom->x_offset += MAX(0, width - geom->width) / 2;
+               geom->width = width;
+           }
+       }
+       else
+       {
+           pango_glyph_string_free(gui.ascii_glyphs);
+           gui.ascii_glyphs = NULL;
+           g_object_unref(gui.ascii_font);
+           gui.ascii_font = NULL;
+       }
+    }
+
+    g_list_foreach(item_list, (GFunc)(void *)&pango_item_free, NULL);
+    g_list_free(item_list);
+    pango_attr_list_unref(attr_list);
+}
+
+    static void
+get_styled_font_variants(void)
+{
+    PangoFontDescription    *bold_font_desc;
+    PangoFont              *plain_font;
+    PangoFont              *bold_font;
+
+    gui.font_can_bold = FALSE;
+
+    plain_font = pango_context_load_font(gui.text_context, gui.norm_font);
+    if (plain_font == NULL)
+       return;
+
+    bold_font_desc = pango_font_description_copy_static(gui.norm_font);
+    pango_font_description_set_weight(bold_font_desc, PANGO_WEIGHT_BOLD);
+
+    bold_font = pango_context_load_font(gui.text_context, bold_font_desc);
+    if (bold_font != NULL)
+    {
+       gui.font_can_bold = (bold_font != plain_font);
+       g_object_unref(bold_font);
+    }
+
+    pango_font_description_free(bold_font_desc);
+    g_object_unref(plain_font);
+}
+
+    int
+gui_mch_init_font(char_u *font_name, int fontset UNUSED)
+{
+    PangoFontDescription    *font_desc;
+    PangoLayout                    *layout;
+    int                            width;
+
+    if (font_name == NULL)
+       font_name = (char_u *)DEFAULT_FONT;
+
+    font_desc = gui_mch_get_font(font_name, FALSE);
+    if (font_desc == NULL)
+       return FAIL;
+
+    gui_mch_free_font(gui.norm_font);
+    gui.norm_font = font_desc;
+
+    pango_context_set_font_description(gui.text_context, font_desc);
+
+    layout = pango_layout_new(gui.text_context);
+    pango_layout_set_text(layout, "MW", 2);
+    pango_layout_get_size(layout, &width, NULL);
+    g_object_unref(layout);
+
+    gui.char_width = (width / 2 + PANGO_SCALE - 1) / PANGO_SCALE;
+    if (gui.char_width <= 0)
+       gui.char_width = 8;
+
+    gui_mch_adjust_charheight();
+
+    hl_set_font_name(font_name);
+
+    get_styled_font_variants();
+    ascii_glyph_table_init();
+
+    return OK;
+}
+
+    GuiFont
+gui_mch_get_font(char_u *name, int report_error)
+{
+    PangoFontDescription *font;
+
+    if (name == NULL)
+       return NULL;
+
+    font = pango_font_description_from_string((const char *)name);
+    if (font == NULL)
+    {
+       if (report_error)
+           semsg(_(e_unknown_font_str), name);
+       return NULL;
+    }
+
+    // Ensure a size is set
+    if (pango_font_description_get_size(font) <= 0)
+       pango_font_description_set_size(font, 10 * PANGO_SCALE);
+
+    return font;
+}
+
+    char_u *
+gui_mch_get_fontname(GuiFont font, char_u *name UNUSED)
+{
+    if (font != NOFONT)
+    {
+       char *desc = pango_font_description_to_string(font);
+       char_u *ret = vim_strsave((char_u *)desc);
+       g_free(desc);
+       return ret;
+    }
+    return NULL;
+}
+
+    void
+gui_mch_free_font(GuiFont font)
+{
+    if (font != NOFONT)
+       pango_font_description_free(font);
+}
+
+    void
+gui_mch_expand_font(
+    optexpand_T        *args,
+    void       *param,
+    int                (*add_match)(char_u *val))
+{
+    PangoFontFamily    **font_families = NULL;
+    int                        n_families = 0;
+    int                        wide = *(int *)param;
+
+    if (args->oe_include_orig_val && *args->oe_opt_value == NUL && !wide)
+    {
+       // If guifont is empty, suggest the default so the user can modify it.
+       if (add_match((char_u *)DEFAULT_FONT) != OK)
+           return;
+    }
+
+    pango_context_list_families(
+           gui.text_context,
+           &font_families,
+           &n_families);
+
+    for (int i = 0; i < n_families; i++)
+    {
+       if (!wide && !pango_font_family_is_monospace(font_families[i]))
+           continue;
+
+       const char *fam_name = pango_font_family_get_name(font_families[i]);
+       if (input_conv.vc_type != CONV_NONE)
+       {
+           char_u *buf = string_convert(&input_conv,
+                   (char_u *)fam_name, NULL);
+           if (buf != NULL)
+           {
+               if (add_match(buf) != OK)
+               {
+                   vim_free(buf);
+                   break;
+               }
+               vim_free(buf);
+           }
+           else
+               break;
+       }
+       else
+       {
+           if (add_match((char_u *)fam_name) != OK)
+               break;
+       }
+    }
+
+    g_free(font_families);
+}
+
+/*
+ * ============================================================
+ * Color handling
+ * ============================================================
+ */
+
+    guicolor_T
+gui_mch_get_color(char_u *name)
+{
+    if (!gui.in_use)
+       return INVALCOLOR;
+
+    if (name != NULL)
+       return gui_get_color_cmn(name);
+
+    return INVALCOLOR;
+}
+
+    guicolor_T
+gui_mch_get_rgb_color(int r, int g, int b)
+{
+    return gui_get_rgb_color_cmn(r, g, b);
+}
+
+    static GdkRGBA
+color_to_rgba(guicolor_T color)
+{
+    GdkRGBA rgba;
+    rgba.red   = ((color & 0xff0000) >> 16) / 255.0;
+    rgba.green = ((color & 0xff00) >> 8) / 255.0;
+    rgba.blue  = (color & 0xff) / 255.0;
+    rgba.alpha = 1.0;
+    return rgba;
+}
+
+    void
+gui_mch_set_fg_color(guicolor_T color)
+{
+    *gui.fgcolor = color_to_rgba(color);
+}
+
+    void
+gui_mch_set_bg_color(guicolor_T color)
+{
+    *gui.bgcolor = color_to_rgba(color);
+}
+
+    void
+gui_mch_set_sp_color(guicolor_T color)
+{
+    *gui.spcolor = color_to_rgba(color);
+}
+
+    guicolor_T
+gui_mch_get_rgb(guicolor_T pixel)
+{
+    return pixel;
+}
+
+/*
+ * ============================================================
+ * Drawing
+ * ============================================================
+ */
+
+static void set_cairo_source_from_pixel(cairo_t *cr, guicolor_T pixel);
+
+    static void
+draw_event(GtkDrawingArea *area UNUSED, cairo_t *cr,
+       int width, int height, gpointer data UNUSED)
+{
+    // Surface creation/resizing is handled by drawarea_resize_cb.
+    // Here we only paint the surface to the widget.
+
+    // Fill background with Vim's background color
+    set_cairo_source_from_pixel(cr, gui.back_pixel);
+    cairo_rectangle(cr, 0, 0, width, height);
+    cairo_fill(cr);
+
+    // Paint the Vim surface on top
+    if (gui.surface != NULL)
+    {
+       cairo_set_source_surface(cr, gui.surface, 0, 0);
+       cairo_paint(cr);
+    }
+}
+
+    static void
+set_cairo_source_from_pixel(cairo_t *cr, guicolor_T pixel)
+{
+    cairo_set_source_rgb(cr,
+           ((pixel & 0xff0000) >> 16) / 255.0,
+           ((pixel & 0xff00) >> 8) / 255.0,
+           (pixel & 0xff) / 255.0);
+}
+
+    void
+gui_mch_clear_block(int row1, int col1, int row2, int col2)
+{
+    cairo_t *cr;
+
+    if (gui.surface == NULL)
+       return;
+
+    cr = cairo_create(gui.surface);
+    set_cairo_source_from_pixel(cr, gui.back_pixel);
+    cairo_rectangle(cr,
+           FILL_X(col1), FILL_Y(row1),
+           (col2 - col1 + 1) * gui.char_width,
+           (row2 - row1 + 1) * gui.char_height);
+    cairo_fill(cr);
+    cairo_destroy(cr);
+
+    if (gui.drawarea != NULL)
+       gtk_widget_queue_draw(gui.drawarea);
+}
+
+    void
+gui_mch_clear_all(void)
+{
+    cairo_t *cr;
+
+    if (gui.surface == NULL)
+       return;
+
+    cr = cairo_create(gui.surface);
+    set_cairo_source_from_pixel(cr, gui.back_pixel);
+    cairo_paint(cr);
+    cairo_destroy(cr);
+
+    if (gui.drawarea != NULL)
+       gtk_widget_queue_draw(gui.drawarea);
+}
+
+    static void
+surface_copy_rect(int dest_x, int dest_y,
+       int src_x, int src_y,
+       int width, int height)
+{
+    cairo_t *cr;
+    cairo_surface_t *tmp;
+
+    if (gui.surface == NULL || width <= 0 || height <= 0)
+       return;
+
+    // Use a temporary surface to avoid overlap issues
+    tmp = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+    cr = cairo_create(tmp);
+    cairo_set_source_surface(cr, gui.surface, -src_x, -src_y);
+    cairo_paint(cr);
+    cairo_destroy(cr);
+
+    cr = cairo_create(gui.surface);
+    cairo_set_source_surface(cr, tmp, dest_x, dest_y);
+    cairo_paint(cr);
+    cairo_destroy(cr);
+    cairo_surface_destroy(tmp);
+}
+
+    void
+gui_mch_delete_lines(int row, int num_lines)
+{
+    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;
+
+    surface_copy_rect(
+           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);
+    gui_clear_block(
+           gui.scroll_region_bot - num_lines + 1, gui.scroll_region_left,
+           gui.scroll_region_bot, gui.scroll_region_right);
+
+    gtk_widget_queue_draw(gui.drawarea);
+}
+
+    void
+gui_mch_insert_lines(int row, int num_lines)
+{
+    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;
+
+    surface_copy_rect(
+           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);
+    gui_clear_block(
+           row, gui.scroll_region_left,
+           row + num_lines - 1, gui.scroll_region_right);
+
+    gtk_widget_queue_draw(gui.drawarea);
+}
+
+    void
+gui_mch_draw_hollow_cursor(guicolor_T color)
+{
+    cairo_t *cr;
+    int i = 1;
+
+    if (gui.surface == NULL)
+       return;
+
+    cr = cairo_create(gui.surface);
+    gui_mch_set_fg_color(color);
+    cairo_set_source_rgba(cr,
+           gui.fgcolor->red, gui.fgcolor->green,
+           gui.fgcolor->blue, gui.fgcolor->alpha);
+    if (mb_lefthalve(gui.row, gui.col))
+       i = 2;
+    cairo_set_line_width(cr, 1.0);
+    cairo_rectangle(cr,
+           FILL_X(gui.col) + 0.5, FILL_Y(gui.row) + 0.5,
+           i * gui.char_width - 1, gui.char_height - 1);
+    cairo_stroke(cr);
+    cairo_destroy(cr);
+
+    gtk_widget_queue_draw(gui.drawarea);
+}
+
+    void
+gui_mch_draw_part_cursor(int w, int h, guicolor_T color)
+{
+    cairo_t *cr;
+
+    if (gui.surface == NULL)
+       return;
+
+    gui_mch_set_fg_color(color);
+    cr = cairo_create(gui.surface);
+    cairo_set_source_rgba(cr,
+           gui.fgcolor->red, gui.fgcolor->green,
+           gui.fgcolor->blue, gui.fgcolor->alpha);
+    cairo_rectangle(cr,
+#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);
+    cairo_fill(cr);
+    cairo_destroy(cr);
+
+    gtk_widget_queue_draw(gui.drawarea);
+}
+
+    void
+gui_mch_flash(int msec)
+{
+    // Invert the screen, wait, then invert back
+    if (gui.surface == NULL)
+       return;
+
+    gui_mch_invert_rectangle(0, 0, (int)Rows - 1, (int)Columns - 1);
+    gui_mch_flush();
+    ui_delay((long)msec, TRUE);
+    gui_mch_invert_rectangle(0, 0, (int)Rows - 1, (int)Columns - 1);
+}
+
+    void
+gui_mch_invert_rectangle(int r, int c, int nr, int nc)
+{
+    cairo_t *cr;
+
+    if (gui.surface == NULL)
+       return;
+
+    cr = cairo_create(gui.surface);
+    cairo_set_operator(cr, CAIRO_OPERATOR_DIFFERENCE);
+    cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
+    cairo_rectangle(cr,
+           FILL_X(c), FILL_Y(r),
+           (nc + 1) * gui.char_width, (nr + 1) * gui.char_height);
+    cairo_fill(cr);
+    cairo_destroy(cr);
+
+    gtk_widget_queue_draw(gui.drawarea);
+}
+
+/*
+ * ============================================================
+ * Event handling
+ * ============================================================
+ */
+
+    static gboolean
+key_press_event(GtkEventControllerKey *controller UNUSED,
+       guint key_sym, guint keycode UNUSED,
+       GdkModifierType state, gpointer data UNUSED)
+{
+    char_u     string[32], string2[32];
+    int                len;
+    int                i;
+    int                modifiers;
+    int                key;
+    char_u     *s, *d;
+
+#ifdef FEAT_XIM
+    // Let the input method have a go at the key event.
+    // If it consumed the event, we're done.
+    if (xic != NULL)
+    {
+       GdkEvent *event = gtk_event_controller_get_current_event(
+               GTK_EVENT_CONTROLLER(controller));
+       if (event != NULL && gtk_im_context_filter_keypress(xic, event))
+           return TRUE;
+    }
+#endif
+
+    len = keyval_to_string(key_sym, string2);
+
+    if (len > 1 && input_conv.vc_type != CONV_NONE)
+       len = convert_input(string2, len, sizeof(string2));
+
+    s = string2;
+    d = string;
+    for (i = 0; i < len; ++i)
+    {
+       *d++ = s[i];
+       if (d[-1] == CSI && d + 2 < string + sizeof(string))
+       {
+           *d++ = KS_EXTRA;
+           *d++ = (int)KE_CSI;
+       }
+    }
+    len = d - string;
+
+    // Shift-Tab results in Left_Tab
+    if (key_sym == GDK_KEY_ISO_Left_Tab)
+    {
+       key_sym = GDK_KEY_Tab;
+       state |= GDK_SHIFT_MASK;
+    }
+
+    // Check for special keys
+    if (len == 0 || len == 1)
+    {
+       for (i = 0; special_keys[i].key_sym != 0; i++)
+       {
+           if (special_keys[i].key_sym == key_sym)
+           {
+               string[0] = CSI;
+               string[1] = special_keys[i].code0;
+               string[2] = special_keys[i].code1;
+               len = -3;
+               break;
+           }
+       }
+    }
+
+    if (len == 0)
+       return TRUE;
+
+    if (len == -3)
+       key = TO_SPECIAL(string[1], string[2]);
+    else
+    {
+       string[len] = NUL;
+       key = mb_ptr2char(string);
+    }
+
+    modifiers = modifiers_gdk2vim(state);
+
+    key = simplify_key(key, &modifiers);
+    if (key == CSI)
+       key = K_CSI;
+    if (IS_SPECIAL(key))
+    {
+       string[0] = CSI;
+       string[1] = K_SECOND(key);
+       string[2] = K_THIRD(key);
+       len = 3;
+    }
+    else
+    {
+       key = may_adjust_key_for_ctrl(modifiers, key);
+       modifiers = may_remove_shift_modifier(modifiers, key);
+       len = mb_char2bytes(key, string);
+    }
+
+    if (modifiers != 0)
+    {
+       string2[0] = CSI;
+       string2[1] = KS_MODIFIER;
+       string2[2] = modifiers;
+       add_to_input_buf(string2, 3);
+    }
+
+    {
+       int int_ch = check_for_interrupt(key, modifiers);
+       if (int_ch != NUL)
+       {
+           trash_input_buf();
+           string[0] = int_ch;
+           len = 1;
+       }
+    }
+
+    add_to_input_buf(string, len);
+
+    if (p_mh)
+       gui_mch_mousehide(TRUE);
+
+    return TRUE;
+}
+
+    static void
+key_release_event(GtkEventControllerKey *controller UNUSED,
+       guint keyval UNUSED, guint keycode UNUSED,
+       GdkModifierType state UNUSED, gpointer data UNUSED)
+{
+}
+
+static int mouse_timed_out = TRUE;
+static guint mouse_click_timer = 0;
+
+    static timeout_cb_type
+mouse_click_timer_cb(gpointer data)
+{
+    *(int *)data = TRUE;
+    return FALSE;
+}
+
+    static int
+modifiers_gdk2mouse(guint state)
+{
+    int modifiers = 0;
+
+    if (state & GDK_SHIFT_MASK)
+       modifiers |= MOUSE_SHIFT;
+    if (state & GDK_CONTROL_MASK)
+       modifiers |= MOUSE_CTRL;
+    if (state & GDK_ALT_MASK)
+       modifiers |= MOUSE_ALT;
+
+    return modifiers;
+}
+
+// Track which mouse button is currently pressed for drag detection.
+// GtkEventControllerMotion's modifier state may not include button masks
+// on all backends (e.g. Wayland), so we track it ourselves.
+// -1 means no button is pressed (MOUSE_LEFT is 0x00, so can't use 0).
+static int mouse_pressed_button = -1;
+
+    static void
+button_press_event(GtkGestureClick *gesture, int n_press UNUSED,
+       double x, double y, gpointer data UNUSED)
+{
+    int button;
+    int repeated_click = FALSE;
+    int_u vim_modifiers;
+    guint btn;
+    GdkModifierType state;
+    GdkEvent *event;
+
+    event = gtk_event_controller_get_current_event(
+           GTK_EVENT_CONTROLLER(gesture));
+    state = gdk_event_get_modifier_state(event);
+    btn = gdk_button_event_get_button(event);
+
+    if (!mouse_timed_out && mouse_click_timer)
+    {
+       timeout_remove(mouse_click_timer);
+       mouse_click_timer = 0;
+       repeated_click = TRUE;
+    }
+
+    mouse_timed_out = FALSE;
+    mouse_click_timer = timeout_add(p_mouset, mouse_click_timer_cb,
+           &mouse_timed_out);
+
+    switch (btn)
+    {
+       case 1: button = MOUSE_LEFT; break;
+       case 2: button = MOUSE_MIDDLE; break;
+       case 3: button = MOUSE_RIGHT; break;
+       case 8: button = MOUSE_X1; break;
+       case 9: button = MOUSE_X2; break;
+       default: return;
+    }
+
+    mouse_pressed_button = button;
+    vim_modifiers = modifiers_gdk2mouse(state);
+    gui_send_mouse_event(button, (int)x, (int)y, repeated_click, vim_modifiers);
+}
+
+    static void
+button_release_event(GtkGestureClick *gesture, int n_press UNUSED,
+       double x, double y, gpointer data UNUSED)
+{
+    int vim_modifiers;
+    GdkModifierType state;
+    GdkEvent *event;
+
+    event = gtk_event_controller_get_current_event(
+           GTK_EVENT_CONTROLLER(gesture));
+    state = gdk_event_get_modifier_state(event);
+    vim_modifiers = modifiers_gdk2mouse(state);
+
+    mouse_pressed_button = -1;
+    gui_send_mouse_event(MOUSE_RELEASE, (int)x, (int)y, FALSE, vim_modifiers);
+}
+
+    static void
+motion_notify_event(GtkEventControllerMotion *controller UNUSED,
+       double x, double y, gpointer data UNUSED)
+{
+    if (mouse_pressed_button >= 0)
+    {
+       GdkModifierType state;
+       GdkEvent *event;
+
+       event = gtk_event_controller_get_current_event(
+               GTK_EVENT_CONTROLLER(controller));
+       if (event != NULL)
+       {
+           state = gdk_event_get_modifier_state(event);
+           gui_send_mouse_event(MOUSE_DRAG, (int)x, (int)y,
+                   FALSE, modifiers_gdk2mouse(state));
+       }
+    }
+
+    if (p_mh)
+       gui_mch_mousehide(FALSE);
+}
+
+    static void
+enter_notify_event(GtkEventControllerMotion *controller UNUSED,
+       double x UNUSED, double y UNUSED, gpointer data UNUSED)
+{
+    if (blink_state == BLINK_NONE)
+       gui_mch_start_blink();
+
+    // Make sure keyboard input goes to the drawing area.
+    if (!gtk_widget_has_focus(gui.drawarea))
+       gtk_widget_grab_focus(gui.drawarea);
+}
+
+    static void
+leave_notify_event(GtkEventControllerMotion *controller UNUSED,
+       gpointer data UNUSED)
+{
+    if (blink_state != BLINK_NONE)
+       gui_mch_stop_blink(TRUE);
+}
+
+    static gboolean
+scroll_event(GtkEventControllerScroll *controller UNUSED,
+       double dx UNUSED, double dy, gpointer data UNUSED)
+{
+    int button;
+    int_u vim_modifiers;
+    GdkModifierType state;
+    GdkEvent *event;
+
+    event = gtk_event_controller_get_current_event(
+           GTK_EVENT_CONTROLLER(controller));
+    if (event == NULL)
+       return FALSE;
+    state = gdk_event_get_modifier_state(event);
+
+    if (dy < 0)
+       button = MOUSE_4;       // scroll up
+    else if (dy > 0)
+       button = MOUSE_5;       // scroll down
+    else if (dx < 0)
+       button = MOUSE_7;       // scroll left
+    else if (dx > 0)
+       button = MOUSE_6;       // scroll right
+    else
+       return FALSE;
+
+    vim_modifiers = modifiers_gdk2mouse(state);
+
+    {
+       double mx, my;
+       gdk_event_get_position(event, &mx, &my);
+       gui_send_mouse_event(button, (int)mx, (int)my, FALSE, vim_modifiers);
+    }
+
+    return TRUE;
+}
+
+    static void
+focus_in_event(GtkEventControllerFocus *controller UNUSED,
+       gpointer data UNUSED)
+{
+    gui_focus_change(TRUE);
+    if (blink_state == BLINK_NONE)
+       gui_mch_start_blink();
+}
+
+    static void
+focus_out_event(GtkEventControllerFocus *controller UNUSED,
+       gpointer data UNUSED)
+{
+    gui_focus_change(FALSE);
+    if (blink_state != BLINK_NONE)
+       gui_mch_stop_blink(TRUE);
+}
+
+    static void
+drawarea_realize_cb(GtkWidget *widget UNUSED, gpointer data UNUSED)
+{
+    int w, h;
+
+    // Use formwin size since drawarea may not have its final size yet
+    if (gui.formwin != NULL)
+    {
+       w = gtk_widget_get_width(gui.formwin);
+       h = gtk_widget_get_height(gui.formwin);
+    }
+    else
+    {
+       w = gtk_widget_get_width(widget);
+       h = gtk_widget_get_height(widget);
+    }
+
+    if (w <= 0) w = 800;
+    if (h <= 0) h = 600;
+
+    if (gui.surface != NULL)
+       cairo_surface_destroy(gui.surface);
+    gui.surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
+
+    gui_mch_new_colors();
+
+#ifdef FEAT_XIM
+    xim_init();
+#endif
+}
+
+    static void
+drawarea_unrealize_cb(GtkWidget *widget UNUSED, gpointer data UNUSED)
+{
+    if (gui.surface != NULL)
+    {
+       cairo_surface_destroy(gui.surface);
+       gui.surface = NULL;
+    }
+}
+
+    static void
+drawarea_resize_cb(GtkDrawingArea *area UNUSED, int width, int height,
+       gpointer data UNUSED)
+{
+    cairo_t *cr;
+
+    if (width <= 0 || height <= 0)
+       return;
+
+    if (gui.surface != NULL)
+    {
+       int sw = cairo_image_surface_get_width(gui.surface);
+       int sh = cairo_image_surface_get_height(gui.surface);
+
+       if (sw == width && sh == height)
+           return;
+
+       cairo_surface_destroy(gui.surface);
+    }
+
+    // Create a fresh surface filled with the background color.
+    // Do not copy old surface content: gui_resize_shell() will trigger
+    // a full redraw, and stale content (e.g. intro screen text) would
+    // otherwise remain as ghost artifacts.
+    gui.surface = cairo_image_surface_create(
+           CAIRO_FORMAT_ARGB32, width, height);
+    cr = cairo_create(gui.surface);
+    set_cairo_source_from_pixel(cr, gui.back_pixel);
+    cairo_paint(cr);
+    cairo_destroy(cr);
+
+    // Notify Vim about the new size - this will cause a full redraw
+    gui_resize_shell(width, height);
+}
+
+#ifdef FEAT_DND
+/*
+ * Drag-and-drop handler for files and text.
+ */
+    static gboolean
+drop_cb(GtkDropTarget *target UNUSED, const GValue *value,
+       double x, double y, gpointer data UNUSED)
+{
+    if (G_VALUE_HOLDS(value, GDK_TYPE_FILE_LIST))
+    {
+       GSList  *files = g_value_get_boxed(value);
+       int     nfiles = g_slist_length(files);
+       char_u  **fnames;
+       int     i;
+
+       if (nfiles <= 0)
+           return FALSE;
+
+       fnames = ALLOC_MULT(char_u *, nfiles);
+       if (fnames == NULL)
+           return FALSE;
+
+       i = 0;
+       for (GSList *l = files; l != NULL; l = l->next)
+       {
+           GFile *file = l->data;
+           char *path = g_file_get_path(file);
+           if (path != NULL)
+               fnames[i++] = vim_strsave((char_u *)path);
+           g_free(path);
+       }
+       nfiles = i;
+
+       if (nfiles > 0)
+           gui_handle_drop((int)x, (int)y, 0, fnames, nfiles);
+       else
+           vim_free(fnames);
+
+       return TRUE;
+    }
+    else if (G_VALUE_HOLDS(value, G_TYPE_STRING))
+    {
+       const char  *text = g_value_get_string(value);
+       char_u      dropkey[6] = {CSI, KS_MODIFIER, 0,
+                                 CSI, KS_EXTRA, (char_u)KE_DROP};
+
+       if (text == NULL || *text == NUL)
+           return FALSE;
+
+       dnd_yank_drag_data((char_u *)text, (long)STRLEN(text));
+       add_to_input_buf(dropkey + 3, 3);
+
+       return TRUE;
+    }
+
+    return FALSE;
+}
+#endif
+
+    static void
+mainwin_destroy_cb(GObject *object UNUSED, gpointer data UNUSED)
+{
+    gui.mainwin = NULL;
+    gui.drawarea = NULL;
+    if (!exiting)
+       gui_shell_closed();
+}
+
+    static gboolean
+delete_event_cb(GtkWindow *window UNUSED, gpointer data UNUSED)
+{
+    gui_shell_closed();
+    return TRUE;
+}
+
+/*
+ * ============================================================
+ * Misc functions
+ * ============================================================
+ */
+
+    static timeout_cb_type
+input_timer_cb(gpointer data)
+{
+    int *timed_out = (int *)data;
+
+    *timed_out = TRUE;
+    return FALSE;      // don't call me again
+}
+
+    void
+gui_mch_update(void)
+{
+    int cnt = 0;
+
+    while (g_main_context_pending(NULL) && !vim_is_input_buf_full()
+                                                               && ++cnt < 100)
+       g_main_context_iteration(NULL, TRUE);
+}
+
+    int
+gui_mch_wait_for_chars(long wtime)
+{
+    int                focus;
+    guint      timer;
+    static int timed_out;
+    int                retval = FAIL;
+
+    timed_out = FALSE;
+
+    if (wtime >= 0)
+       timer = timeout_add(wtime == 0 ? 1L : wtime,
+                                                  input_timer_cb, &timed_out);
+    else
+       timer = 0;
+
+    focus = gui.in_focus;
+
+    do
+    {
+       // Stop or start blinking when focus changes
+       if (gui.in_focus != focus)
+       {
+           if (gui.in_focus)
+               gui_mch_start_blink();
+           else
+               gui_mch_stop_blink(TRUE);
+           focus = gui.in_focus;
+       }
+
+#ifdef MESSAGE_QUEUE
+# ifdef FEAT_TIMERS
+       did_add_timer = FALSE;
+# endif
+       parse_queued_messages();
+# ifdef FEAT_TIMERS
+       if (did_add_timer)
+           goto theend;
+# endif
+#endif
+
+       if (gui.mainwin == NULL)
+           goto theend;
+
+       // gtk_main_quit() is a wake-up request; consume it so later
+       // waits resume.
+       if (gtk4_main_loop_quit)
+       {
+           gtk4_main_loop_quit = FALSE;
+           goto theend;
+       }
+
+       if (!input_available())
+       {
+           ++gtk4_main_loop_level;
+           g_main_context_iteration(NULL, TRUE);
+           --gtk4_main_loop_level;
+       }
+
+       if (input_available())
+       {
+           retval = OK;
+           goto theend;
+       }
+    } while (wtime < 0 || !timed_out);
+
+    gui_mch_update();
+
+theend:
+    if (timer != 0 && !timed_out)
+       timeout_remove(timer);
+
+    return retval;
+}
+
+    void
+gui_mch_flush(void)
+{
+    // Ensure the offscreen surface content gets painted to the widget.
+    if (gui.drawarea != NULL)
+       gtk_widget_queue_draw(gui.drawarea);
+    if (gui.mainwin != NULL && gtk_widget_get_realized(gui.mainwin))
+       gdk_display_flush(gtk_widget_get_display(gui.mainwin));
+}
+
+    void
+gui_mch_beep(void)
+{
+    GdkDisplay *display;
+
+    if (gui.mainwin != NULL && gtk_widget_get_realized(gui.mainwin))
+    {
+       display = gtk_widget_get_display(gui.mainwin);
+       if (display != NULL)
+           gdk_display_beep(display);
+    }
+}
+
+    void *
+gui_mch_get_display(void)
+{
+    if (gui.mainwin != NULL && gtk_widget_get_display(gui.mainwin))
+       return gtk_widget_get_display(gui.mainwin);
+    return NULL;
+}
+
+    void
+gui_mch_iconify(void)
+{
+    gtk_window_minimize(GTK_WINDOW(gui.mainwin));
+}
+
+    void
+gui_mch_set_foreground(void)
+{
+    gtk_window_present(GTK_WINDOW(gui.mainwin));
+}
+
+    void
+gui_mch_getmouse(int *x, int *y)
+{
+    *x = 0;
+    *y = 0;
+    // GTK4: No reliable way to query pointer position synchronously.
+}
+
+    void
+gui_mch_setmouse(int x UNUSED, int y UNUSED)
+{
+    // GTK4/Wayland: cannot warp pointer
+}
+
+    void
+gui_mch_mousehide(int hide)
+{
+    if (gui.pointer_hidden == hide)
+       return;
+
+    gui.pointer_hidden = hide;
+    if (gui.blank_pointer != NULL)
+    {
+       if (hide)
+           gtk_widget_set_cursor(gui.drawarea, gui.blank_pointer);
+       else
+#ifdef FEAT_MOUSESHAPE
+           mch_set_mouse_shape(last_shape);
+#else
+           gtk_widget_set_cursor(gui.drawarea, NULL);
+#endif
+    }
+}
+
+    int
+gui_mch_haskey(char_u *name)
+{
+    int i;
+
+    for (i = 0; special_keys[i].key_sym != 0; i++)
+       if (name[0] == special_keys[i].code0
+               && name[1] == special_keys[i].code1)
+           return OK;
+    return FAIL;
+}
+
+    void
+gui_mch_forked(void)
+{
+}
+
+/*
+ * ============================================================
+ * Scrollbar
+ * ============================================================
+ */
+
+    void
+gui_mch_enable_scrollbar(scrollbar_T *sb, int flag)
+{
+    if (sb->id != NULL)
+       gtk_widget_set_visible(sb->id, flag);
+}
+
+/*
+ * ============================================================
+ * Menu stubs
+ * ============================================================
+ */
+
+    void
+gui_mch_menu_grey(vimmenu_T *menu, int grey)
+{
+    if (menu->id == NULL || menu_action_group == NULL)
+       return;
+
+    // For toolbar items, use gtk_widget_set_sensitive
+    if (menu->parent != NULL && menu_is_toolbar(menu->parent->name))
+    {
+       if (menu->id != (GtkWidget *)1)
+           gtk_widget_set_sensitive(menu->id, !grey);
+       return;
+    }
+
+    // For menu items, enable/disable the GSimpleAction
+    if (menu->label != NULL)
+    {
+       GAction *action = g_action_map_lookup_action(
+               G_ACTION_MAP(menu_action_group),
+               (const char *)menu->label);
+       if (action != NULL)
+           g_simple_action_set_enabled(G_SIMPLE_ACTION(action), !grey);
+    }
+}
+
+    void
+gui_mch_menu_hidden(vimmenu_T *menu UNUSED, int hidden UNUSED)
+{
+    // No-op: menu system not yet implemented for GTK4.
+}
+
+    void
+gui_mch_draw_menubar(void)
+{
+    // No-op: menu system not yet implemented for GTK4.
+}
+
+/*
+ * ============================================================
+ * Tabline
+ * ============================================================
+ */
+
+#ifdef FEAT_GUI_TABLINE
+    void
+gui_mch_show_tabline(int showit)
+{
+    if (gui.tabline != NULL)
+       gtk_widget_set_visible(gui.tabline, showit);
+}
+
+    int
+gui_mch_showing_tabline(void)
+{
+    return gui.tabline != NULL && gtk_widget_get_visible(gui.tabline);
+}
+
+static int ignore_tabline_evt = FALSE;
+
+    void
+gui_mch_update_tabline(void)
+{
+    GtkWidget  *page;
+    GtkWidget  *event_box;
+    GtkWidget  *label;
+    tabpage_T  *tp;
+    int                nr = 0;
+    int                tab_num;
+    int                curtabidx = 0;
+    char_u     *labeltext;
+
+    if (gui.tabline == NULL)
+       return;
+
+    ignore_tabline_evt = TRUE;
+
+    for (tp = first_tabpage; tp != NULL; tp = tp->tp_next, ++nr)
+    {
+       if (tp == curtab)
+           curtabidx = nr;
+
+       tab_num = nr + 1;
+
+       page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(gui.tabline), nr);
+       if (page == NULL)
+       {
+           page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+           gtk_widget_set_visible(page, TRUE);
+           event_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+           gtk_widget_set_visible(event_box, TRUE);
+           label = gtk_label_new("-Empty-");
+           gtk_box_append(GTK_BOX(event_box), label);
+           gtk_widget_set_visible(label, TRUE);
+           gtk_notebook_insert_page(GTK_NOTEBOOK(gui.tabline),
+                   page, event_box, nr++);
+           gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(gui.tabline),
+                   page, TRUE);
+       }
+
+       event_box = gtk_notebook_get_tab_label(GTK_NOTEBOOK(gui.tabline), page);
+       g_object_set_data(G_OBJECT(event_box), "tab_num",
+               GINT_TO_POINTER(tab_num));
+       label = gtk_widget_get_first_child(event_box);
+       get_tabline_label(tp, FALSE);
+       labeltext = CONVERT_TO_UTF8(NameBuff);
+       if (label != NULL && GTK_IS_LABEL(label))
+           gtk_label_set_text(GTK_LABEL(label), (const char *)labeltext);
+       CONVERT_TO_UTF8_FREE(labeltext);
+
+       get_tabline_label(tp, TRUE);
+       labeltext = CONVERT_TO_UTF8(NameBuff);
+       gtk_widget_set_tooltip_text(event_box, (const gchar *)labeltext);
+       CONVERT_TO_UTF8_FREE(labeltext);
+    }
+
+    while (gtk_notebook_get_nth_page(GTK_NOTEBOOK(gui.tabline), nr) != NULL)
+       gtk_notebook_remove_page(GTK_NOTEBOOK(gui.tabline), nr);
+
+    if (gtk_notebook_get_current_page(GTK_NOTEBOOK(gui.tabline)) != curtabidx)
+       gtk_notebook_set_current_page(GTK_NOTEBOOK(gui.tabline), curtabidx);
+
+    ignore_tabline_evt = FALSE;
+}
+
+    void
+gui_mch_set_curtab(int nr)
+{
+    if (gui.tabline != NULL)
+       gtk_notebook_set_current_page(GTK_NOTEBOOK(gui.tabline), nr - 1);
+}
+#endif
+
+/*
+ * ============================================================
+ * Sign support
+ * ============================================================
+ */
+
+#if defined(FEAT_SIGN_ICONS)
+# define SIGN_WIDTH  (2 * gui.char_width)
+# define SIGN_HEIGHT (gui.char_height)
+
+    void
+gui_mch_drawsign(int row, int col, int typenr)
+{
+    GdkPixbuf  *sign;
+    cairo_t    *cr;
+    int                width, height;
+
+    sign = (GdkPixbuf *)sign_get_image(typenr);
+    if (sign == NULL || gui.surface == NULL)
+       return;
+
+    cr = cairo_create(gui.surface);
+
+    width = gdk_pixbuf_get_width(sign);
+    height = gdk_pixbuf_get_height(sign);
+
+    // Scale to fit the sign area if needed
+    if (width != SIGN_WIDTH || height != SIGN_HEIGHT)
+    {
+       GdkPixbuf *scaled = gdk_pixbuf_scale_simple(sign,
+               SIGN_WIDTH, SIGN_HEIGHT, GDK_INTERP_BILINEAR);
+       if (scaled != NULL)
+       {
+           gdk_cairo_set_source_pixbuf(cr, scaled,
+                   FILL_X(col), FILL_Y(row));
+           g_object_unref(scaled);
+       }
+       else
+           gdk_cairo_set_source_pixbuf(cr, sign,
+                   FILL_X(col), FILL_Y(row));
+    }
+    else
+       gdk_cairo_set_source_pixbuf(cr, sign,
+               FILL_X(col), FILL_Y(row));
+
+    cairo_paint(cr);
+    cairo_destroy(cr);
+
+    gtk_widget_queue_draw(gui.drawarea);
+}
+
+    void *
+gui_mch_register_sign(char_u *signfile)
+{
+    if (signfile[0] != NUL && signfile[0] != '-' && gui.in_use)
+    {
+       GdkPixbuf   *sign;
+       GError      *error = NULL;
+
+       sign = gdk_pixbuf_new_from_file((const char *)signfile, &error);
+       if (error == NULL)
+           return sign;
+
+       semsg("E255: %s", error->message);
+       g_error_free(error);
+    }
+    return NULL;
+}
+
+    void
+gui_mch_destroy_sign(void *sign)
+{
+    if (sign != NULL)
+       g_object_unref(sign);
+}
+#endif
+
+/*
+ * ============================================================
+ * Stubs for functions not yet implemented or not applicable in GTK4
+ * ============================================================
+ */
+
+/*
+ * Ligature and text drawing support.
+ * Ported from gui_gtk_x11.c (GTK3) to support 'guiligatures' in GTK4.
+ */
+
+#define INSERT_PANGO_ATTR(Attribute, AttrList, Start, End)  \
+    G_STMT_START{                                          \
+       PangoAttribute *tmp_attr_;                          \
+       tmp_attr_ = (Attribute);                            \
+       tmp_attr_->start_index = (Start);                   \
+       tmp_attr_->end_index = (End);                       \
+       pango_attr_list_insert((AttrList), tmp_attr_);      \
+    }G_STMT_END
+
+/*
+ * Apply the 'guifontwide' font to double-width characters in the string.
+ */
+    static void
+apply_wide_font_attr(char_u *s, int len, PangoAttrList *attr_list)
+{
+    char_u  *start = NULL;
+    char_u  *p;
+    int            uc;
+
+    for (p = s; p < s + len; p += utf_byte2len(*p))
+    {
+       uc = utf_ptr2char(p);
+
+       if (start == NULL)
+       {
+           if (uc >= 0x80 && utf_char2cells(uc) == 2)
+               start = p;
+       }
+       else if (uc < 0x80
+                || (utf_char2cells(uc) != 2 && !utf_iscomposing(uc)))
+       {
+           INSERT_PANGO_ATTR(pango_attr_font_desc_new(gui.wide_font),
+                             attr_list, start - s, p - s);
+           start = NULL;
+       }
+    }
+
+    if (start != NULL)
+       INSERT_PANGO_ATTR(pango_attr_font_desc_new(gui.wide_font),
+                         attr_list, start - s, len);
+}
+
+/*
+ * Count the number of display cells occupied by a glyph cluster.
+ */
+    static int
+count_cluster_cells(char_u *s, PangoItem *item,
+                   PangoGlyphString *glyphs, int i,
+                   int *cluster_width,
+                   int *last_glyph_rbearing)
+{
+    char_u  *p;
+    int            next;
+    int            start, end;
+    int            width;
+    int            uc;
+    int            cellcount = 0;
+
+    width = glyphs->glyphs[i].geometry.width;
+
+    for (next = i + 1; next < glyphs->num_glyphs; ++next)
+    {
+       if (glyphs->glyphs[next].attr.is_cluster_start)
+           break;
+       else if (glyphs->glyphs[next].geometry.width > width)
+           width = glyphs->glyphs[next].geometry.width;
+    }
+
+    start = item->offset + glyphs->log_clusters[i];
+    end   = item->offset + ((next < glyphs->num_glyphs) ?
+                           glyphs->log_clusters[next] : item->length);
+
+    for (p = s + start; p < s + end; p += utf_byte2len(*p))
+    {
+       uc = utf_ptr2char(p);
+       if (uc < 0x80)
+           ++cellcount;
+       else if (!utf_iscomposing(uc))
+           cellcount += utf_char2cells(uc);
+    }
+
+    if (last_glyph_rbearing != NULL
+           && cellcount > 0 && next == glyphs->num_glyphs)
+    {
+       PangoRectangle ink_rect;
+
+       pango_font_get_glyph_extents(item->analysis.font,
+                                    glyphs->glyphs[i].glyph,
+                                    &ink_rect, NULL);
+
+       if (PANGO_RBEARING(ink_rect) > 0)
+           *last_glyph_rbearing = PANGO_RBEARING(ink_rect);
+    }
+
+    if (cellcount > 0)
+       *cluster_width = width;
+
+    return cellcount;
+}
+
+/*
+ * Handle combining characters that form a zero-width cluster.
+ */
+    static void
+setup_zero_width_cluster(PangoItem *item, PangoGlyphInfo *glyph,
+                        int last_cellcount, int last_cluster_width,
+                        int last_glyph_rbearing)
+{
+    PangoRectangle  ink_rect;
+    PangoRectangle  logical_rect;
+    int                    width;
+
+    width = last_cellcount * gui.char_width * PANGO_SCALE;
+    glyph->geometry.x_offset = -width + MAX(0, width - last_cluster_width) / 2;
+    glyph->geometry.width = 0;
+
+    pango_font_get_glyph_extents(item->analysis.font,
+                                glyph->glyph,
+                                &ink_rect, &logical_rect);
+    if (ink_rect.x < 0)
+    {
+       glyph->geometry.x_offset += last_glyph_rbearing;
+       glyph->geometry.y_offset  = logical_rect.height
+               - (gui.char_height - p_linespace) * PANGO_SCALE;
+    }
+    else
+       glyph->geometry.x_offset = -width + MAX(0, width - ink_rect.width) / 2;
+}
+
+/*
+ * Draw a single glyph string segment: background, foreground, and fake bold.
+ */
+    static void
+draw_glyph_string(int row, int col, int num_cells, int flags,
+                 PangoFont *font, PangoGlyphString *glyphs,
+                 cairo_t *cr)
+{
+    if (!(flags & DRAW_TRANSP))
+    {
+       cairo_set_source_rgba(cr,
+               gui.bgcolor->red, gui.bgcolor->green, gui.bgcolor->blue,
+               gui.bgcolor->alpha);
+       cairo_rectangle(cr,
+                       FILL_X(col), FILL_Y(row),
+                       num_cells * gui.char_width, gui.char_height);
+       cairo_fill(cr);
+    }
+
+    cairo_set_source_rgba(cr,
+           gui.fgcolor->red, gui.fgcolor->green, gui.fgcolor->blue,
+           gui.fgcolor->alpha);
+    cairo_move_to(cr, TEXT_X(col), TEXT_Y(row));
+    pango_cairo_show_glyph_string(cr, font, glyphs);
+
+    // Redraw with offset of 1 to emulate bold
+    if ((flags & DRAW_BOLD) && !gui.font_can_bold)
+    {
+       cairo_move_to(cr, TEXT_X(col) + 1, TEXT_Y(row));
+       pango_cairo_show_glyph_string(cr, font, glyphs);
+    }
+}
+
+/*
+ * Draw underline, undercurl, and strikethrough decorations.
+ */
+    static void
+draw_under(int flags, int row, int col, int cells, cairo_t *cr)
+{
+    // Draw underline
+    if (flags & DRAW_UNDERL)
+    {
+       int y = FILL_Y(row + 1) - 1;
+       cairo_set_source_rgba(cr,
+               gui.fgcolor->red, gui.fgcolor->green,
+               gui.fgcolor->blue, gui.fgcolor->alpha);
+       cairo_set_line_width(cr, 1.0);
+       cairo_move_to(cr, FILL_X(col), y + 0.5);
+       cairo_line_to(cr, FILL_X(col + cells), y + 0.5);
+       cairo_stroke(cr);
+    }
+
+    // Draw undercurl
+    if (flags & DRAW_UNDERC)
+    {
+       static const int val[8] = {1, 0, 0, 0, 1, 2, 2, 2};
+       int y = FILL_Y(row + 1) - 1;
+       int i, offset;
+
+       cairo_set_line_width(cr, 1.0);
+       cairo_set_source_rgba(cr,
+               gui.spcolor->red, gui.spcolor->green,
+               gui.spcolor->blue, gui.spcolor->alpha);
+       cairo_move_to(cr, FILL_X(col) + 1, y - 2 + 0.5);
+       for (i = FILL_X(col) + 1; i < FILL_X(col + cells); ++i)
+       {
+           offset = val[i % 8];
+           cairo_line_to(cr, i, y - offset + 0.5);
+       }
+       cairo_stroke(cr);
+    }
+
+    // Draw strikethrough
+    if (flags & DRAW_STRIKE)
+    {
+       int y = FILL_Y(row) + gui.char_height / 2;
+       cairo_set_source_rgba(cr,
+               gui.fgcolor->red, gui.fgcolor->green,
+               gui.fgcolor->blue, gui.fgcolor->alpha);
+       cairo_set_line_width(cr, 1.0);
+       cairo_move_to(cr, FILL_X(col), y + 0.5);
+       cairo_line_to(cr, FILL_X(col + cells), y + 0.5);
+       cairo_stroke(cr);
+    }
+}
+
+/*
+ * Draw a string of characters on the screen.
+ * "force_pango" is set when ligature characters require Pango shaping
+ * instead of the fast ASCII glyph cache path.
+ * Returns the number of display cells used.
+ */
+    int
+gui_gtk_draw_string_ext(
+       int     row,
+       int     col,
+       char_u  *s,
+       int     len,
+       int     flags,
+       int     force_pango)
+{
+    GdkRectangle       area;
+    PangoGlyphString   *glyphs;
+    int                        column_offset = 0;
+    int                        i;
+    cairo_t            *cr;
+
+    if (gui.text_context == NULL || gui.surface == NULL)
+       return len;
+
+    // Restrict all drawing to the current screen line.
+    area.x = gui.border_offset;
+    area.y = FILL_Y(row);
+    area.width = gui.num_cols * gui.char_width;
+    area.height = gui.char_height;
+
+    cr = cairo_create(gui.surface);
+    cairo_rectangle(cr, area.x, area.y, area.width, area.height);
+    cairo_clip(cr);
+
+    glyphs = pango_glyph_string_new();
+
+    // Fast path for pure ASCII: use cached glyph table.
+    // Skip this path when force_pango is set (ligatures need shaping).
+    if (!(flags & DRAW_ITALIC)
+           && !((flags & DRAW_BOLD) && gui.font_can_bold)
+           && gui.ascii_glyphs != NULL
+           && !force_pango)
+    {
+       char_u *p;
+
+       for (p = s; p < s + len; ++p)
+           if (*p & 0x80)
+               goto not_ascii;
+
+       pango_glyph_string_set_size(glyphs, len);
+
+       for (i = 0; i < len; ++i)
+       {
+           glyphs->glyphs[i] = gui.ascii_glyphs->glyphs[2 * s[i]];
+           glyphs->log_clusters[i] = i;
+       }
+
+       draw_glyph_string(row, col, len, flags, gui.ascii_font, glyphs, cr);
+
+       column_offset = len;
+    }
+    else
+not_ascii:
+    {
+       PangoAttrList   *attr_list;
+       GList           *item_list;
+       int             cluster_width;
+       int             last_glyph_rbearing;
+       int             cells = 0;
+
+       // Safety check: pango crashes with invalid utf-8.
+       if (!utf_valid_string(s, s + len))
+       {
+           column_offset = len;
+           goto skipitall;
+       }
+
+       cluster_width = PANGO_SCALE * gui.char_width;
+       last_glyph_rbearing = PANGO_SCALE * gui.char_width;
+
+       attr_list = pango_attr_list_new();
+
+       // If 'guifontwide' is set then use that for double-width characters.
+       if (gui.wide_font != NULL)
+           apply_wide_font_attr(s, len, attr_list);
+
+       if ((flags & DRAW_BOLD) && gui.font_can_bold)
+           INSERT_PANGO_ATTR(pango_attr_weight_new(PANGO_WEIGHT_BOLD),
+                             attr_list, 0, len);
+       if (flags & DRAW_ITALIC)
+           INSERT_PANGO_ATTR(pango_attr_style_new(PANGO_STYLE_ITALIC),
+                             attr_list, 0, len);
+
+       item_list = pango_itemize(gui.text_context,
+                                 (const char *)s, 0, len, attr_list, NULL);
+
+       while (item_list != NULL)
+       {
+           PangoItem   *item;
+           int         item_cells = 0;
+
+           item = (PangoItem *)item_list->data;
+           item_list = g_list_delete_link(item_list, item_list);
+
+           // Force LTR direction; Vim handles bidi on its own.
+           item->analysis.level = (item->analysis.level + 1) & (~1U);
+
+           pango_shape_full((const char *)s + item->offset, item->length,
+                   (const char *)s, len, &item->analysis, glyphs);
+
+           // Fixed-width hack: assign a fixed width to each glyph based on
+           // the number of cells it occupies, handling composing characters
+           // and cluster boundaries properly.
+           for (i = 0; i < glyphs->num_glyphs; ++i)
+           {
+               PangoGlyphInfo *glyph;
+
+               glyph = &glyphs->glyphs[i];
+
+               if (glyph->attr.is_cluster_start)
+               {
+                   int cellcount;
+
+                   cellcount = count_cluster_cells(
+                           s, item, glyphs, i, &cluster_width,
+                           (item_list != NULL) ? &last_glyph_rbearing : NULL);
+
+                   if (cellcount > 0)
+                   {
+                       int width;
+
+                       width = cellcount * gui.char_width * PANGO_SCALE;
+                       glyph->geometry.x_offset +=
+                                           MAX(0, width - cluster_width) / 2;
+                       glyph->geometry.width = width;
+                   }
+                   else
+                   {
+                       setup_zero_width_cluster(item, glyph, cells,
+                                                cluster_width,
+                                                last_glyph_rbearing);
+                   }
+
+                   item_cells += cellcount;
+                   cells = cellcount;
+               }
+               else if (i > 0)
+               {
+                   int width;
+
+                   if (glyph->geometry.x_offset >= 0)
+                   {
+                       glyphs->glyphs[i].geometry.width =
+                                        glyphs->glyphs[i - 1].geometry.width;
+                       glyphs->glyphs[i - 1].geometry.width = 0;
+                   }
+                   width = cells * gui.char_width * PANGO_SCALE;
+                   glyph->geometry.x_offset +=
+                                           MAX(0, width - cluster_width) / 2;
+               }
+               else
+               {
+                   glyph->geometry.width = 0;
+               }
+           }
+
+           draw_glyph_string(row, col + column_offset, item_cells,
+                             flags, item->analysis.font, glyphs, cr);
+
+           pango_item_free(item);
+
+           column_offset += item_cells;
+       }
+
+       pango_attr_list_unref(attr_list);
+    }
+
+skipitall:
+    draw_under(flags, row, col, column_offset, cr);
+
+    pango_glyph_string_free(glyphs);
+
+    cairo_destroy(cr);
+
+    if (gui.drawarea != NULL)
+       gtk_widget_queue_draw(gui.drawarea);
+
+    return column_offset;
+}
+
+/*
+ * Draw a string of characters on the screen using the current font and colors.
+ * Splits the string into ASCII and ligature/UTF-8 segments so that ligature
+ * characters are sent through Pango for proper shaping, while plain ASCII
+ * uses the fast cached glyph path.
+ * Returns the number of display cells used.
+ */
+    int
+gui_gtk_draw_string(int row, int col, char_u *s, int len, int flags)
+{
+    char_u     *conv_buf = NULL;
+    int                convlen;
+    int                len_sum;
+    int                byte_sum;
+    char_u     *cs;
+    int                needs_pango;
+    int                should_need_pango = FALSE;
+    int                slen;
+    int                is_ligature;
+    int                is_utf8;
+    char_u     backup_ch;
+
+    if (gui.text_context == NULL || gui.surface == NULL)
+       return len;
+
+    if (output_conv.vc_type != CONV_NONE)
+    {
+       convlen = len;
+       conv_buf = string_convert(&output_conv, s, &convlen);
+       if (conv_buf != NULL)
+       {
+           s = conv_buf;
+           len = convlen;
+       }
+    }
+
+    /*
+     * Ligature support:
+     * Split the string into segments that are either pure ASCII (fast path)
+     * or ligature/UTF-8 (Pango path).  A single ligature character between
+     * ASCII characters is treated as ASCII since it can't form a ligature
+     * on its own.
+     */
+    len_sum = 0;
+    byte_sum = 0;
+    cs = s;
+
+    // First char decides starting mode.
+    is_utf8 = (*cs & 0x80);
+    is_ligature = gui.ligatures_map[*cs] && (len > 1);
+    if (is_ligature)
+       is_ligature = gui.ligatures_map[*(cs + 1)];
+    if (!is_utf8 && len > 1)
+       is_utf8 = (*(cs + 1) & 0x80) != 0;
+    needs_pango = is_utf8 || is_ligature;
+
+    while (cs < s + len)
+    {
+       slen = 0;
+       while (slen < (len - byte_sum))
+       {
+           is_ligature = gui.ligatures_map[*(cs + slen)];
+           // Look ahead: single ligature char between ASCII is ASCII.
+           if (is_ligature && !needs_pango)
+           {
+               if ((slen + 1) < (len - byte_sum))
+                   is_ligature = gui.ligatures_map[*(cs + slen + 1)];
+               else
+                   is_ligature = 0;
+           }
+           is_utf8 = *(cs + slen) & 0x80;
+           // ASCII followed by UTF-8 could be combining.
+           if ((!is_utf8) && ((slen + 1) < (len - byte_sum)))
+               is_utf8 = (*(cs + slen + 1) & 0x80);
+           should_need_pango = (is_ligature || is_utf8);
+           if (needs_pango != should_need_pango)
+               break;
+           if (needs_pango)
+           {
+               if (is_ligature)
+               {
+                   slen++;
+               }
+               else
+               {
+                   if ((*(cs + slen) & 0xC0) == 0x80)
+                   {
+                       while ((slen < (len - byte_sum))
+                                           && ((*(cs + slen) & 0xC0) == 0x80))
+                           slen++;
+                   }
+                   else if ((*(cs + slen) & 0xE0) == 0xC0)
+                       slen++;
+                   else if ((*(cs + slen) & 0xF0) == 0xE0)
+                       slen += 2;
+                   else if ((*(cs + slen) & 0xF8) == 0xF0)
+                       slen += 3;
+                   else
+                       slen++;
+               }
+           }
+           else
+           {
+               slen++;
+           }
+       }
+
+       if (slen < len)
+       {
+           backup_ch = *(cs + slen);
+           *(cs + slen) = NUL;
+       }
+       len_sum += gui_gtk_draw_string_ext(row, col + len_sum, cs, slen,
+                                           flags, needs_pango);
+       if (slen < len)
+           *(cs + slen) = backup_ch;
+       cs += slen;
+       byte_sum += slen;
+       needs_pango = should_need_pango;
+    }
+    vim_free(conv_buf);
+    return len_sum;
+}
+
+    int
+gui_get_x11_windis(Window *win UNUSED, Display **dis UNUSED)
+{
+    // GTK4: not applicable
+    return FAIL;
+}
+
+#if defined(FEAT_SOCKETSERVER)
+
+/*
+ * Callback for new events from the socket server listening socket.
+ */
+    static int
+socket_server_poll_in(int fd UNUSED, GIOCondition cond,
+                     void *user_data UNUSED)
+{
+    if (cond & G_IO_IN)
+       socket_server_accept_client();
+    else if (cond & (G_IO_ERR | G_IO_HUP))
+    {
+       socket_server_uninit();
+       return FALSE;
+    }
+
+    return TRUE;
+}
+
+#endif // FEAT_SOCKETSERVER
+
+/*
+ * Initialize socket server for use in the GUI (does not actually initialize
+ * the socket server, only attaches a source).
+ */
+    void
+gui_gtk_init_socket_server(void)
+{
+#if defined(FEAT_SOCKETSERVER)
+    if (socket_server_source_id > 0)
+       return;
+    // Register source for file descriptor to global default context
+    socket_server_source_id = g_unix_fd_add(socket_server_get_fd(),
+           G_IO_IN | G_IO_ERR | G_IO_HUP, socket_server_poll_in, NULL);
+#endif
+}
+
+/*
+ * Remove the source for the socket server listening socket.
+ */
+    void
+gui_gtk_uninit_socket_server(void)
+{
+#if defined(FEAT_SOCKETSERVER)
+    if (socket_server_source_id > 0)
+    {
+       g_source_remove(socket_server_source_id);
+       socket_server_source_id = 0;
+    }
+#endif
+}
+
+    void
+gui_gtk_set_mnemonics(int enable UNUSED)
+{
+    // No-op: menu mnemonics depend on menu system, not yet implemented
+    // for GTK4.
+}
+
+    void
+gui_make_popup(char_u *path_name UNUSED, int mouse_pos UNUSED)
+{
+    // No-op: popup menus depend on menu system, not yet implemented
+    // for GTK4.
+}
+
+    int
+get_menu_tool_width(void)
+{
+    return 0;
+}
+
+    int
+get_menu_tool_height(void)
+{
+    int height = 0;
+
+#ifdef FEAT_MENU
+    if (gui.menubar != NULL && gtk_widget_get_visible(gui.menubar))
+    {
+       GtkRequisition req;
+       gtk_widget_get_preferred_size(gui.menubar, &req, NULL);
+       height += req.height;
+    }
+#endif
+#ifdef FEAT_TOOLBAR
+    if (gui.toolbar != NULL && gtk_widget_get_visible(gui.toolbar))
+    {
+       GtkRequisition req;
+       gtk_widget_get_preferred_size(gui.toolbar, &req, NULL);
+       height += req.height;
+    }
+#endif
+    return height;
+}
+
+/*
+ * Get the GdkClipboard for the given Clipboard_T.
+ * clip_star (*) uses PRIMARY, clip_plus (+) uses CLIPBOARD.
+ */
+    static GdkClipboard *
+gtk4_get_clipboard(Clipboard_T *cbd)
+{
+    GdkDisplay *display;
+
+    if (gui.mainwin == NULL)
+       return NULL;
+
+    display = gtk_widget_get_display(gui.mainwin);
+    if (display == NULL)
+       return NULL;
+
+    if (cbd == &clip_plus)
+       return gdk_display_get_clipboard(display);
+    else
+       return gdk_display_get_primary_clipboard(display);
+}
+
+typedef struct {
+    Clipboard_T *cbd;
+    gboolean   done;
+} ClipReadData;
+
+/*
+ * Callback for gdk_clipboard_read_text_async().
+ */
+    static void
+clip_read_text_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    GdkClipboard       *clipboard = GDK_CLIPBOARD(source);
+    ClipReadData       *crd = (ClipReadData *)user_data;
+    Clipboard_T                *cbd = crd->cbd;
+    char               *text;
+    GError             *error = NULL;
+
+    text = gdk_clipboard_read_text_finish(clipboard, result, &error);
+    if (text != NULL)
+    {
+       char_u  *tmpbuf = NULL;
+       char_u  *p;
+       int     len;
+       int     motion_type = MAUTO;
+
+       len = (int)STRLEN(text);
+
+       // Convert from UTF-8 to 'encoding' if needed.
+       if (input_conv.vc_type != CONV_NONE)
+       {
+           tmpbuf = string_convert(&input_conv, (char_u *)text, &len);
+           if (tmpbuf != NULL)
+               p = tmpbuf;
+           else
+               p = (char_u *)text;
+       }
+       else
+           p = (char_u *)text;
+
+       // Chop off any trailing NUL bytes.
+       while (len > 0 && p[len - 1] == NUL)
+           --len;
+
+       clip_yank_selection(motion_type, p, (long)len, cbd);
+       vim_free(tmpbuf);
+       g_free(text);
+    }
+    else
+    {
+       if (error != NULL)
+           g_error_free(error);
+    }
+    crd->done = TRUE;
+}
+
+/*
+ * Request the selection from the clipboard.
+ */
+    void
+clip_mch_request_selection(Clipboard_T *cbd)
+{
+    GdkClipboard       *clipboard;
+    ClipReadData       crd;
+    time_t             start;
+
+    clipboard = gtk4_get_clipboard(cbd);
+    if (clipboard == NULL)
+       return;
+
+    crd.cbd = cbd;
+    crd.done = FALSE;
+    gdk_clipboard_read_text_async(clipboard, NULL, clip_read_text_cb, &crd);
+
+    // Spin until the async callback fires, with a 3-second wall-clock
+    // timeout as a safety net.
+    start = time(NULL);
+    while (!crd.done && time(NULL) < start + 3)
+       g_main_context_iteration(NULL, TRUE);
+}
+
+/*
+ * Send the current selection to the clipboard.
+ */
+    void
+clip_mch_set_selection(Clipboard_T *cbd)
+{
+    GdkClipboard       *clipboard;
+    char_u             *str = NULL;
+    long_u             len;
+    int                        motion_type;
+
+    clipboard = gtk4_get_clipboard(cbd);
+    if (clipboard == NULL)
+       return;
+
+    // Get the selection text from the register.
+    clip_get_selection(cbd);
+    motion_type = clip_convert_selection(&str, &len, cbd);
+    if (motion_type < 0 || str == NULL)
+       return;
+
+    // Convert from 'encoding' to UTF-8 if needed.
+    if (output_conv.vc_type != CONV_NONE)
+    {
+       char_u  *conv_str;
+       int     conv_len = (int)len;
+
+       conv_str = string_convert(&output_conv, str, &conv_len);
+       if (conv_str != NULL)
+       {
+           vim_free(str);
+           str = conv_str;
+           len = conv_len;
+       }
+    }
+
+    // Ensure NUL-terminated string for GTK.
+    {
+       char_u *nul_str = alloc(len + 1);
+
+       if (nul_str != NULL)
+       {
+           mch_memmove(nul_str, str, len);
+           nul_str[len] = NUL;
+           gdk_clipboard_set_text(clipboard, (const char *)nul_str);
+           vim_free(nul_str);
+       }
+    }
+
+    vim_free(str);
+}
+
+/*
+ * Own the selection.  In GTK4, ownership is implicit when content is set
+ * on the clipboard.  Return OK to indicate we can own it.
+ */
+    int
+clip_mch_own_selection(Clipboard_T *cbd UNUSED)
+{
+    return OK;
+}
+
+/*
+ * Disown the selection.  In GTK4, we clear the clipboard content to
+ * release ownership.
+ */
+    void
+clip_mch_lose_selection(Clipboard_T *cbd)
+{
+    GdkClipboard *clipboard;
+
+    clipboard = gtk4_get_clipboard(cbd);
+    if (clipboard == NULL)
+       return;
+
+    // Setting NULL content provider releases ownership.
+    gdk_clipboard_set_content(clipboard, NULL);
+}
+
+// Balloon eval - use GTK4 tooltip
+    void
+gui_mch_post_balloon(BalloonEval *beval UNUSED, char_u *mesg)
+{
+    if (mesg != NULL && gui.drawarea != NULL)
+    {
+       char_u *text = CONVERT_TO_UTF8(mesg);
+       gtk_widget_set_tooltip_text(gui.drawarea, (const char *)text);
+       CONVERT_TO_UTF8_FREE(text);
+    }
+    else if (gui.drawarea != NULL)
+       gtk_widget_set_tooltip_text(gui.drawarea, NULL);
+}
+
+    BalloonEval *
+gui_mch_create_beval_area(void *target UNUSED, char_u *mesg UNUSED,
+       void (*mesgCB)(BalloonEval *, int) UNUSED, void *clientData UNUSED)
+{
+    return NULL;
+}
+
+    void
+gui_mch_enable_beval_area(BalloonEval *beval UNUSED)
+{
+}
+
+    void
+gui_mch_disable_beval_area(BalloonEval *beval UNUSED)
+{
+}
+
+// GTK4 does not have gtk_main_level/gtk_main_quit.
+// Provide compatibility stubs using a simple flag.
+    guint
+gtk_main_level(void)
+{
+    return gtk4_main_loop_level;
+}
+
+    void
+gtk_main_quit(void)
+{
+    gtk4_main_loop_quit = TRUE;
+}
+
+#if defined(FEAT_MOUSESHAPE)
+
+// Table of CSS cursor names corresponding to Vim's mouse shape IDs.
+// Keep in sync with the mshape_names[] table in misc2.c.
+static const char *mshape_css_names[] =
+{
+    "default",                 // arrow
+    "none",                    // blank
+    "text",                    // beam
+    "ns-resize",               // updown
+    "nwse-resize",             // udsizing
+    "ew-resize",               // leftright
+    "ew-resize",               // lrsizing
+    "progress",                        // busy
+    "not-allowed",             // no
+    "crosshair",               // crosshair
+    "pointer",                 // hand1
+    "pointer",                 // hand2
+    "default",                 // pencil (no CSS analogue)
+    "help",                    // question
+    "default",                 // right-arrow (no CSS analogue)
+    "default",                 // up-arrow (no CSS analogue)
+    "default"                  // last entry
+};
+
+    void
+mch_set_mouse_shape(int shape)
+{
+    GdkCursor  *c;
+    const char *css_name = "default";
+
+    if (gui.drawarea == NULL)
+       return;
+
+    if (shape == MSHAPE_HIDE || gui.pointer_hidden)
+       gtk_widget_set_cursor(gui.drawarea, gui.blank_pointer);
+    else
+    {
+       if (shape >= MSHAPE_NUMBERED)
+           css_name = "default";
+       else if (shape < (int)ARRAY_LENGTH(mshape_css_names))
+           css_name = mshape_css_names[shape];
+       else
+           return;
+
+       // GTK4: gdk_cursor_new_from_name(name, fallback)
+       c = gdk_cursor_new_from_name(css_name, NULL);
+       gtk_widget_set_cursor(gui.drawarea, c);
+       g_object_unref(G_OBJECT(c));
+    }
+    if (shape != MSHAPE_HIDE)
+       last_shape = shape;
+}
+
+#else // !FEAT_MOUSESHAPE
+
+    void
+mch_set_mouse_shape(int shape UNUSED)
+{
+}
+
+#endif // FEAT_MOUSESHAPE
+
+
+
+/*
+ * Menus, scrollbars, dialogs, toolbar.
+ * (merged from gui_gtk4.c)
+ */
+
+
+
+static int last_text_area_w = 0;
+static int last_text_area_h = 0;
+
+/*
+ * ============================================================
+ * Menu functions
+ * ============================================================
+ * TODO: Implement using GMenu + GtkPopoverMenuBar
+ */
+
+/*
+ * Icon name table for toolbar buttons.
+ * Must match toolbar_names[] in menu.c.
+ */
+static const char * const toolbar_icon_names[] =
+{
+    /* 00 */ "document-new",
+    /* 01 */ "document-open",
+    /* 02 */ "document-save",
+    /* 03 */ "edit-undo",
+    /* 04 */ "edit-redo",
+    /* 05 */ "edit-cut",
+    /* 06 */ "edit-copy",
+    /* 07 */ "edit-paste",
+    /* 08 */ "document-print",
+    /* 09 */ "help-browser",
+    /* 10 */ "edit-find",
+    /* 11 */ "document-save",          // save all (no standard icon)
+    /* 12 */ "document-save",          // session save
+    /* 13 */ "document-new",           // session new
+    /* 14 */ "document-open",          // session load
+    /* 15 */ "system-run",
+    /* 16 */ "edit-find-replace",
+    /* 17 */ "window-close",
+    /* 18 */ "window-maximize-symbolic",       // maximize
+    /* 19 */ "window-minimize-symbolic",       // minimize
+    /* 20 */ "window-maximize-symbolic",       // split (no standard icon)
+    /* 21 */ "utilities-terminal",     // shell
+    /* 22 */ "go-previous",
+    /* 23 */ "go-next",
+    /* 24 */ "help-browser",           // find help
+    /* 25 */ "edit-find",              // convert (no standard icon)
+    /* 26 */ "go-jump",
+    /* 27 */ "go-previous",            // back (reuse)
+    /* 28 */ "go-next",                        // forward (reuse)
+    /* 29 */ "image-missing",
+    /* 30 */ "image-missing",
+};
+
+    static void
+toolbar_button_clicked_cb(GtkWidget *widget UNUSED, gpointer data)
+{
+    gui_menu_cb((vimmenu_T *)data);
+}
+
+    static GtkWidget *
+create_toolbar_icon(vimmenu_T *menu)
+{
+    char_u     buf[MAXPATHL];
+    GtkWidget  *image = NULL;
+
+    // Try specified icon file first
+    if (menu->iconfile != NULL)
+    {
+       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_for_pixbuf(pixbuf);
+               image = gtk_image_new_from_paintable(
+                       GDK_PAINTABLE(texture));
+               g_object_unref(texture);
+               g_object_unref(pixbuf);
+           }
+       }
+    }
+
+    // Use themed icon
+    if (image == NULL)
+    {
+       const char *icon_name = "image-missing";
+       int n = (int)ARRAY_LENGTH(toolbar_icon_names);
+
+       if (menu->iconidx >= 0 && menu->iconidx < n)
+           icon_name = toolbar_icon_names[menu->iconidx];
+
+       image = gtk_image_new_from_icon_name(icon_name);
+    }
+
+    return image;
+}
+
+/*
+ * GTK4 Menu system using GMenu + GSimpleActionGroup + GtkPopoverMenuBar.
+ *
+ * Each menu/submenu has a GMenu stored in menu->submenu_id (cast to
+ * GtkWidget* to fit the struct field type).
+ * Actions are added to a GSimpleActionGroup attached to gui.mainwin.
+ */
+
+static int menu_action_id = 0;
+
+    static void
+menu_action_cb(GSimpleAction *action UNUSED, GVariant *parameter UNUSED,
+       gpointer data)
+{
+    // Force-close any open popover menus in the menubar.
+    // GTK4 marks them as not-visible but Vim's custom main loop
+    // may not process the rendering update, so we flush explicitly.
+    if (gui.menubar != NULL)
+    {
+       GtkWidget *item;
+
+       for (item = gtk_widget_get_first_child(gui.menubar);
+               item != NULL;
+               item = gtk_widget_get_next_sibling(item))
+       {
+           GtkWidget *child;
+
+           for (child = gtk_widget_get_first_child(item);
+                   child != NULL;
+                   child = gtk_widget_get_next_sibling(child))
+           {
+               if (GTK_IS_POPOVER(child))
+                   gtk_popover_popdown(GTK_POPOVER(child));
+           }
+       }
+    }
+
+    gui_menu_cb((vimmenu_T *)data);
+    gui_mch_flush();
+}
+
+    static char *
+make_action_name(vimmenu_T *menu)
+{
+    // Create a unique action name from the menu pointer
+    static char buf[64];
+    vim_snprintf(buf, sizeof(buf), "menu%d", menu_action_id++);
+    return buf;
+}
+
+    void
+gui_mch_add_menu(vimmenu_T *menu, int idx UNUSED)
+{
+    GMenu *submenu;
+
+    if (menu->name[0] == ']' || menu_is_popup(menu->name))
+    {
+       // Popup menus - just create a GMenu, don't add to menubar
+       submenu = g_menu_new();
+       menu->submenu_id = (GtkWidget *)(gpointer)submenu;
+       return;
+    }
+
+    if (menu->parent != NULL && menu->parent->submenu_id == NULL)
+       return;
+    if (!menu_is_menubar(menu->name))
+       return;
+
+    // Create a submenu for this menu
+    submenu = g_menu_new();
+    menu->submenu_id = (GtkWidget *)(gpointer)submenu;
+
+    // Add to parent menu or menubar's model
+    {
+       GMenu *parent_menu;
+       char_u *label;
+
+       label = CONVERT_TO_UTF8(menu->dname);
+
+       if (menu->parent != NULL)
+           parent_menu = (GMenu *)(gpointer)menu->parent->submenu_id;
+       else
+           parent_menu = (GMenu *)(gpointer)g_object_get_data(
+                   G_OBJECT(gui.menubar), "vim-gmenu");
+
+       if (parent_menu != NULL)
+           g_menu_append_submenu(parent_menu, (const char *)label,
+                   G_MENU_MODEL(submenu));
+
+       CONVERT_TO_UTF8_FREE(label);
+    }
+}
+
+    void
+gui_mch_add_menu_item(vimmenu_T *menu, int idx UNUSED)
+{
+    vimmenu_T *parent = menu->parent;
+
+#ifdef FEAT_TOOLBAR
+    if (parent != NULL && menu_is_toolbar(parent->name))
+    {
+       if (menu_is_separator(menu->name))
+       {
+           GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_VERTICAL);
+           gtk_box_append(GTK_BOX(gui.toolbar), sep);
+           menu->id = sep;
+       }
+       else
+       {
+           GtkWidget   *btn;
+           GtkWidget   *icon;
+           char_u      *tooltip;
+
+           icon = create_toolbar_icon(menu);
+           btn = gtk_button_new();
+           gtk_button_set_child(GTK_BUTTON(btn), icon);
+           gtk_widget_set_focusable(btn, FALSE);
+           gtk_widget_add_css_class(btn, "flat");
+
+           tooltip = CONVERT_TO_UTF8(menu->strings[MENU_INDEX_TIP]);
+           if (tooltip != NULL && utf_valid_string(tooltip, NULL))
+               gtk_widget_set_tooltip_text(btn, (const gchar *)tooltip);
+           CONVERT_TO_UTF8_FREE(tooltip);
+
+           g_signal_connect(btn, "clicked",
+                   G_CALLBACK(toolbar_button_clicked_cb), menu);
+
+           gtk_box_append(GTK_BOX(gui.toolbar), btn);
+           menu->id = btn;
+       }
+       return;
+    }
+#endif
+
+    // Menu items (non-toolbar)
+    if (parent == NULL || parent->submenu_id == NULL)
+       return;
+
+    {
+       GMenu *parent_menu = (GMenu *)(gpointer)parent->submenu_id;
+
+       if (menu_is_separator(menu->name))
+       {
+           // GMenu doesn't have real separators; use a section
+           GMenu *section = g_menu_new();
+           g_menu_append_section(parent_menu, NULL, G_MENU_MODEL(section));
+           g_object_unref(section);
+           menu->id = NULL;
+       }
+       else
+       {
+           char        *action_name;
+           char        detailed[80];
+           char_u      *label;
+           GSimpleAction *action;
+
+           // Create a unique action
+           action_name = make_action_name(menu);
+           action = g_simple_action_new(action_name, NULL);
+           g_signal_connect(action, "activate",
+                   G_CALLBACK(menu_action_cb), menu);
+
+           if (menu_action_group == NULL)
+           {
+               menu_action_group = g_simple_action_group_new();
+               gtk_widget_insert_action_group(gui.mainwin, "menu",
+                       G_ACTION_GROUP(menu_action_group));
+           }
+           g_action_map_add_action(G_ACTION_MAP(menu_action_group),
+                   G_ACTION(action));
+           g_object_unref(action);
+
+           label = CONVERT_TO_UTF8(menu->dname);
+           vim_snprintf(detailed, sizeof(detailed), "menu.%s", action_name);
+           g_menu_append(parent_menu, (const char *)label, detailed);
+           CONVERT_TO_UTF8_FREE(label);
+
+           menu->id = (GtkWidget *)1;  // non-NULL marker
+           // Store action name for later use (grey/enable)
+           menu->label = (GtkWidget *)vim_strsave(
+                   (char_u *)action_name);
+       }
+    }
+}
+
+    void
+gui_mch_toggle_tearoffs(int enable UNUSED)
+{
+    // GTK4: tearoff menus don't exist.
+}
+
+    void
+gui_mch_menu_set_tip(vimmenu_T *menu UNUSED)
+{
+}
+
+    void
+gui_mch_destroy_menu(vimmenu_T *menu)
+{
+    // For toolbar buttons, remove from toolbar
+    if (menu->id != NULL && menu->id != (GtkWidget *)1)
+    {
+       GtkWidget *parent_widget = gtk_widget_get_parent(menu->id);
+       if (parent_widget != NULL)
+           gtk_box_remove(GTK_BOX(parent_widget), menu->id);
+       menu->id = NULL;
+    }
+    else
+       menu->id = NULL;
+
+    // Free stored action name
+    vim_free(menu->label);
+    menu->label = NULL;
+
+    // GMenu items cannot be individually removed easily.
+    // The submenu GMenu is unreffed if present.
+    if (menu->submenu_id != NULL)
+    {
+       // Don't unref - GMenu may be referenced by the model
+       menu->submenu_id = NULL;
+    }
+}
+
+    static void
+popupmenu_closed_cb(GtkPopover *popover, gpointer data UNUSED)
+{
+    gtk_widget_unparent(GTK_WIDGET(popover));
+}
+
+    void
+gui_mch_show_popupmenu(vimmenu_T *menu)
+{
+    GMenu *gmenu;
+    GtkWidget *popover;
+
+    if (menu == NULL || menu->submenu_id == NULL)
+       return;
+
+    gmenu = (GMenu *)(gpointer)menu->submenu_id;
+    popover = gtk_popover_menu_new_from_model(G_MENU_MODEL(gmenu));
+    gtk_widget_set_parent(popover, gui.drawarea);
+    g_signal_connect(popover, "closed",
+           G_CALLBACK(popupmenu_closed_cb), NULL);
+    gtk_popover_popup(GTK_POPOVER(popover));
+}
+
+/*
+ * ============================================================
+ * Scrollbar functions
+ * ============================================================
+ */
+
+    void
+gui_mch_set_scrollbar_thumb(scrollbar_T *sb, long val, long size, long max)
+{
+    GtkAdjustment *adj;
+
+    if (sb->id == NULL)
+       return;
+    if (!GTK_IS_WIDGET(sb->id) || !GTK_IS_RANGE(sb->id))
+       return;
+
+    adj = gtk_range_get_adjustment(GTK_RANGE(sb->id));
+    gtk_adjustment_set_lower(adj, 0.0);
+    gtk_adjustment_set_upper(adj, (gdouble)max + 1);
+    gtk_adjustment_set_value(adj, (gdouble)val);
+    gtk_adjustment_set_step_increment(adj, 1.0);
+    gtk_adjustment_set_page_increment(adj, (gdouble)(size > 2 ? size - 2 : 1));
+    gtk_adjustment_set_page_size(adj, (gdouble)size);
+}
+
+    void
+gui_mch_set_scrollbar_pos(scrollbar_T *sb, int x, int y, int w, int h)
+{
+    if (sb->id != NULL)
+    {
+       gtk_widget_set_size_request(sb->id, w, h);
+       gui_gtk_form_move(GTK_FORM(gui.formwin), sb->id, x, y);
+    }
+}
+
+    int
+gui_mch_get_scrollbar_xpadding(void)
+{
+    int formwin_w = gtk_widget_get_width(gui.formwin);
+    int sbar_w = 0;
+    int xpad;
+
+    if (gui.which_scrollbars[SBAR_LEFT])
+       sbar_w += gui.scrollbar_width;
+    if (gui.which_scrollbars[SBAR_RIGHT])
+       sbar_w += gui.scrollbar_width;
+
+    xpad = formwin_w - last_text_area_w - sbar_w;
+    return (xpad < 0) ? 0 : xpad;
+}
+
+    int
+gui_mch_get_scrollbar_ypadding(void)
+{
+    int formwin_h = gtk_widget_get_height(gui.formwin);
+    int ypad;
+
+    ypad = formwin_h - last_text_area_h;
+    if (gui.which_scrollbars[SBAR_BOTTOM])
+       ypad -= gui.scrollbar_height;
+
+    return (ypad < 0) ? 0 : ypad;
+}
+
+    static void
+adjustment_value_changed(GtkAdjustment *adj, gpointer data UNUSED)
+{
+    scrollbar_T *sb = (scrollbar_T *)g_object_get_data(G_OBJECT(adj), "vim-sb");
+    long value = (long)gtk_adjustment_get_value(adj);
+
+    if (sb != NULL)
+       gui_drag_scrollbar(sb, value, FALSE);
+}
+
+    void
+gui_mch_create_scrollbar(scrollbar_T *sb, int orient)
+{
+    if (orient == SBAR_HORIZ)
+       sb->id = gtk_scrollbar_new(GTK_ORIENTATION_HORIZONTAL, NULL);
+    else
+       sb->id = gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL, NULL);
+
+    if (sb->id != NULL && GTK_IS_RANGE(sb->id))
+    {
+       GtkAdjustment *adj = gtk_range_get_adjustment(GTK_RANGE(sb->id));
+
+       gtk_widget_set_visible(sb->id, FALSE);
+       gui_gtk_form_put(GTK_FORM(gui.formwin), sb->id, 0, 0);
+       if (adj != NULL && G_IS_OBJECT(adj))
+       {
+           g_object_set_data(G_OBJECT(adj), "vim-sb", (gpointer)sb);
+           g_signal_connect(G_OBJECT(adj), "value-changed",
+                   G_CALLBACK(adjustment_value_changed), NULL);
+       }
+    }
+}
+
+    void
+gui_mch_destroy_scrollbar(scrollbar_T *sb)
+{
+    if (sb->id != NULL)
+    {
+       gui_gtk_form_remove(GTK_FORM(gui.formwin), sb->id);
+       sb->id = NULL;
+    }
+}
+
+/*
+ * ============================================================
+ * Text area position
+ * ============================================================
+ */
+
+    void
+gui_mch_set_text_area_pos(int x, int y, int w, int h)
+{
+    last_text_area_w = w;
+    last_text_area_h = h;
+    // Don't use gui_gtk_form_move_resize for drawarea because its
+    // set_size_request would prevent the window from shrinking.
+    // Just update position; the actual allocation is handled by
+    // form_size_allocate which gives drawarea the formwin's full size.
+    gui_gtk_form_move(GTK_FORM(gui.formwin), gui.drawarea, x, y);
+
+    // Update surface to match new text area size
+    if (w > 0 && h > 0)
+    {
+       if (gui.surface != NULL)
+       {
+           int sw = cairo_image_surface_get_width(gui.surface);
+           int sh = cairo_image_surface_get_height(gui.surface);
+           if (sw == w && sh == h)
+               return;
+           cairo_surface_destroy(gui.surface);
+       }
+       gui.surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
+    }
+}
+
+/*
+ * ============================================================
+ * Browse dialogs
+ * ============================================================
+ */
+
+/*
+ * Blocking helper: run a GtkFileDialog and wait for result.
+ */
+typedef struct {
+    GFile      *result;
+    gboolean   done;
+} FileDialogData;
+
+    static void
+file_dialog_open_cb(GObject *source, GAsyncResult *res, gpointer data)
+{
+    FileDialogData *fdd = (FileDialogData *)data;
+    fdd->result = gtk_file_dialog_open_finish(
+                   GTK_FILE_DIALOG(source), res, NULL);
+    fdd->done = TRUE;
+}
+
+    static void
+file_dialog_save_cb(GObject *source, GAsyncResult *res, gpointer data)
+{
+    FileDialogData *fdd = (FileDialogData *)data;
+    fdd->result = gtk_file_dialog_save_finish(
+                   GTK_FILE_DIALOG(source), res, NULL);
+    fdd->done = TRUE;
+}
+
+    static void
+file_dialog_folder_cb(GObject *source, GAsyncResult *res, gpointer data)
+{
+    FileDialogData *fdd = (FileDialogData *)data;
+    fdd->result = gtk_file_dialog_select_folder_finish(
+                   GTK_FILE_DIALOG(source), res, NULL);
+    fdd->done = TRUE;
+}
+
+    char_u *
+gui_mch_browse(int saving,
+       char_u *title,
+       char_u *dflt,
+       char_u *ext UNUSED,
+       char_u *initdir,
+       char_u *filter UNUSED)
+{
+    GtkFileDialog      *dlg;
+    FileDialogData     fdd;
+    char_u             dirbuf[MAXPATHL];
+    char_u             *result = NULL;
+
+    title = CONVERT_TO_UTF8(title);
+
+    if (initdir == NULL || *initdir == NUL)
+       mch_dirname(dirbuf, MAXPATHL);
+    else if (vim_FullName(initdir, dirbuf, MAXPATHL - 2, FALSE) == FAIL)
+       dirbuf[0] = NUL;
+    add_pathsep(dirbuf);
+
+    gui_mch_mousehide(FALSE);
+
+    dlg = gtk_file_dialog_new();
+    gtk_file_dialog_set_modal(dlg, TRUE);
+    if (title != NULL)
+       gtk_file_dialog_set_title(dlg, (const char *)title);
+
+    {
+       GFile *dir = g_file_new_for_path((const char *)dirbuf);
+       gtk_file_dialog_set_initial_folder(dlg, dir);
+       g_object_unref(dir);
+    }
+
+    if (saving && dflt != NULL && *dflt != NUL)
+       gtk_file_dialog_set_initial_name(dlg, (const char *)dflt);
+
+    fdd.result = NULL;
+    fdd.done = FALSE;
+
+    if (saving)
+       gtk_file_dialog_save(dlg, GTK_WINDOW(gui.mainwin), NULL,
+               file_dialog_save_cb, &fdd);
+    else
+       gtk_file_dialog_open(dlg, GTK_WINDOW(gui.mainwin), NULL,
+               file_dialog_open_cb, &fdd);
+
+    while (!fdd.done)
+       g_main_context_iteration(NULL, TRUE);
+
+    if (fdd.result != NULL)
+    {
+       char *path = g_file_get_path(fdd.result);
+       if (path != NULL)
+       {
+           result = vim_strsave((char_u *)path);
+           g_free(path);
+       }
+       g_object_unref(fdd.result);
+    }
+
+    g_object_unref(dlg);
+    CONVERT_TO_UTF8_FREE(title);
+
+    return result;
+}
+
+    char_u *
+gui_mch_browsedir(char_u *title, char_u *initdir)
+{
+    GtkFileDialog      *dlg;
+    FileDialogData     fdd;
+    char_u             *result = NULL;
+
+    title = CONVERT_TO_UTF8(title);
+    gui_mch_mousehide(FALSE);
+
+    dlg = gtk_file_dialog_new();
+    gtk_file_dialog_set_modal(dlg, TRUE);
+    if (title != NULL)
+       gtk_file_dialog_set_title(dlg, (const char *)title);
+
+    if (initdir != NULL && *initdir != NUL)
+    {
+       GFile *dir = g_file_new_for_path((const char *)initdir);
+       gtk_file_dialog_set_initial_folder(dlg, dir);
+       g_object_unref(dir);
+    }
+
+    fdd.result = NULL;
+    fdd.done = FALSE;
+
+    gtk_file_dialog_select_folder(dlg, GTK_WINDOW(gui.mainwin), NULL,
+           file_dialog_folder_cb, &fdd);
+
+    while (!fdd.done)
+       g_main_context_iteration(NULL, TRUE);
+
+    if (fdd.result != NULL)
+    {
+       char *path = g_file_get_path(fdd.result);
+       if (path != NULL)
+       {
+           result = vim_strsave((char_u *)path);
+           g_free(path);
+       }
+       g_object_unref(fdd.result);
+    }
+
+    g_object_unref(dlg);
+    CONVERT_TO_UTF8_FREE(title);
+
+    return result;
+}
+
+/*
+ * ============================================================
+ * Message dialog
+ * ============================================================
+ */
+
+typedef struct {
+    int                response;
+    gboolean   done;
+} AlertDialogData;
+
+    static void
+alert_dialog_cb(GObject *source, GAsyncResult *res, gpointer data)
+{
+    AlertDialogData *add = (AlertDialogData *)data;
+    add->response = gtk_alert_dialog_choose_finish(
+                   GTK_ALERT_DIALOG(source), res, NULL);
+    add->done = TRUE;
+}
+
+    int
+gui_mch_dialog(
+       int     type UNUSED,
+       char_u  *title,
+       char_u  *message,
+       char_u  *buttons,
+       int     dfltbutton,
+       char_u  *textfield UNUSED,
+       int     ex_cmd UNUSED)
+{
+    GtkAlertDialog     *dlg;
+    AlertDialogData    add;
+    char_u             *p;
+    char_u             *buf = NULL;
+    int                        butcount = 0;
+    int                        i;
+    const char         *btn_labels[64];
+    char_u             *btn_conv[64];
+
+    title = CONVERT_TO_UTF8(title);
+    message = CONVERT_TO_UTF8(message);
+
+    // Parse button labels from the "&Yes\n&No\n&Cancel" format
+    if (buttons != NULL)
+    {
+       buf = vim_strsave(buttons);
+       if (buf != NULL)
+       {
+           p = buf;
+           while (*p != NUL && butcount < 63)
+           {
+               char_u *start = p;
+               while (*p != NUL && *p != '\n')
+                   ++p;
+               if (*p == '\n')
+                   *p++ = NUL;
+               // Skip '&' mnemonic marker
+               if (*start == '&')
+                   ++start;
+               btn_conv[butcount] = CONVERT_TO_UTF8(start);
+               btn_labels[butcount] = (const char *)btn_conv[butcount];
+               butcount++;
+           }
+       }
+    }
+    btn_labels[butcount] = NULL;
+
+    dlg = gtk_alert_dialog_new("%s", message ? (char *)message : "");
+    if (title != NULL)
+       gtk_alert_dialog_set_detail(dlg, (const char *)title);
+    gtk_alert_dialog_set_buttons(dlg, btn_labels);
+    gtk_alert_dialog_set_modal(dlg, TRUE);
+
+    if (dfltbutton > 0 && dfltbutton <= butcount)
+       gtk_alert_dialog_set_default_button(dlg, dfltbutton - 1);
+    if (butcount > 0)
+       gtk_alert_dialog_set_cancel_button(dlg, butcount - 1);
+
+    add.response = -1;
+    add.done = FALSE;
+
+    gtk_alert_dialog_choose(dlg, GTK_WINDOW(gui.mainwin), NULL,
+           alert_dialog_cb, &add);
+
+    while (!add.done)
+       g_main_context_iteration(NULL, TRUE);
+
+    g_object_unref(dlg);
+
+    for (i = 0; i < butcount; i++)
+       CONVERT_TO_UTF8_FREE(btn_conv[i]);
+    vim_free(buf);
+    CONVERT_TO_UTF8_FREE(title);
+    CONVERT_TO_UTF8_FREE(message);
+
+    // GTK returns 0-based index, Vim wants 1-based
+    return add.response >= 0 ? add.response + 1 : 0;
+}
+
+/*
+ * ============================================================
+ * Find/Replace dialogs
+ * ============================================================
+ */
+
+/*
+ * ============================================================
+ * Find/Replace dialog
+ * ============================================================
+ */
+
+typedef struct
+{
+    GtkWidget *dialog;
+    GtkWidget *what;       // Find what entry
+    GtkWidget *with;       // Replace with entry
+    GtkWidget *wword;      // Whole word check
+    GtkWidget *mcase;      // Match case check
+    GtkWidget *up;         // Direction up radio
+    GtkWidget *down;       // Direction down radio
+} SharedFindReplace;
+
+static SharedFindReplace find_widgets = {0};
+static SharedFindReplace repl_widgets = {0};
+
+    static void
+find_replace_cb(GtkWidget *widget UNUSED, gpointer data)
+{
+    int                        flags;
+    char_u             *find_text;
+    char_u             *repl_text;
+    gboolean           direction_down;
+    SharedFindReplace  *sfr;
+
+    flags = GPOINTER_TO_INT(data);
+
+    if (flags == FRD_FINDNEXT)
+    {
+       repl_text = NULL;
+       sfr = &find_widgets;
+    }
+    else
+    {
+       repl_text = (char_u *)gtk_editable_get_text(
+               GTK_EDITABLE(repl_widgets.with));
+       sfr = &repl_widgets;
+    }
+
+    find_text = (char_u *)gtk_editable_get_text(GTK_EDITABLE(sfr->what));
+    direction_down = gtk_check_button_get_active(
+           GTK_CHECK_BUTTON(sfr->down));
+
+    if (gtk_check_button_get_active(GTK_CHECK_BUTTON(sfr->wword)))
+       flags |= FRD_WHOLE_WORD;
+    if (gtk_check_button_get_active(GTK_CHECK_BUTTON(sfr->mcase)))
+       flags |= FRD_MATCH_CASE;
+
+    repl_text = CONVERT_FROM_UTF8(repl_text);
+    find_text = CONVERT_FROM_UTF8(find_text);
+    gui_do_findrepl(flags, find_text, repl_text, direction_down);
+    CONVERT_FROM_UTF8_FREE(repl_text);
+    CONVERT_FROM_UTF8_FREE(find_text);
+}
+
+    static void
+dialog_destroyed_cb(GtkWidget *widget UNUSED, gpointer data)
+{
+    *(GtkWidget **)data = NULL;
+}
+
+    static void
+find_replace_dialog_create(char_u *arg, int do_replace)
+{
+    SharedFindReplace  *frdp;
+    char_u             *entry_text;
+    int                        wword = FALSE;
+    int                        mcase = !p_ic;
+    GtkWidget          *vbox, *grid, *hbox, *tmp, *btn;
+    gboolean           sensitive;
+
+    frdp = do_replace ? &repl_widgets : &find_widgets;
+    entry_text = get_find_dialog_text(arg, &wword, &mcase);
+
+    if (entry_text != NULL && output_conv.vc_type != CONV_NONE)
+    {
+       char_u *old = entry_text;
+       entry_text = string_convert(&output_conv, entry_text, NULL);
+       vim_free(old);
+    }
+
+    // If the dialog already exists, just raise it.
+    if (frdp->dialog)
+    {
+       if (entry_text != NULL)
+       {
+           gtk_editable_set_text(GTK_EDITABLE(frdp->what),
+                   (char *)entry_text);
+           gtk_check_button_set_active(GTK_CHECK_BUTTON(frdp->wword),
+                   (gboolean)wword);
+           gtk_check_button_set_active(GTK_CHECK_BUTTON(frdp->mcase),
+                   (gboolean)mcase);
+       }
+       gtk_window_present(GTK_WINDOW(frdp->dialog));
+       gtk_widget_grab_focus(frdp->what);
+       vim_free(entry_text);
+       return;
+    }
+
+    // Create a new dialog window.
+    frdp->dialog = gtk_window_new();
+    gtk_window_set_transient_for(GTK_WINDOW(frdp->dialog),
+           GTK_WINDOW(gui.mainwin));
+    gtk_window_set_destroy_with_parent(GTK_WINDOW(frdp->dialog), TRUE);
+    gtk_window_set_title(GTK_WINDOW(frdp->dialog),
+           do_replace ? _("VIM - Search and Replace...")
+                      : _("VIM - Search..."));
+    gtk_window_set_resizable(GTK_WINDOW(frdp->dialog), FALSE);
+
+    g_signal_connect(frdp->dialog, "destroy",
+           G_CALLBACK(dialog_destroyed_cb), &frdp->dialog);
+
+    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
+    gtk_widget_set_margin_start(vbox, 12);
+    gtk_widget_set_margin_end(vbox, 12);
+    gtk_widget_set_margin_top(vbox, 12);
+    gtk_widget_set_margin_bottom(vbox, 12);
+    gtk_window_set_child(GTK_WINDOW(frdp->dialog), vbox);
+
+    // Grid for labels + entries
+    grid = gtk_grid_new();
+    gtk_grid_set_row_spacing(GTK_GRID(grid), 6);
+    gtk_grid_set_column_spacing(GTK_GRID(grid), 6);
+    gtk_box_append(GTK_BOX(vbox), grid);
+
+    // "Find what:" label + entry
+    tmp = gtk_label_new(_("Find what:"));
+    gtk_label_set_xalign(GTK_LABEL(tmp), 0.0);
+    gtk_grid_attach(GTK_GRID(grid), tmp, 0, 0, 1, 1);
+
+    frdp->what = gtk_entry_new();
+    gtk_widget_set_hexpand(frdp->what, TRUE);
+    sensitive = (entry_text != NULL && entry_text[0] != NUL);
+    if (entry_text != NULL)
+       gtk_editable_set_text(GTK_EDITABLE(frdp->what), (char *)entry_text);
+    gtk_grid_attach(GTK_GRID(grid), frdp->what, 1, 0, 1, 1);
+
+    if (do_replace)
+    {
+       // "Replace with:" label + entry
+       tmp = gtk_label_new(_("Replace with:"));
+       gtk_label_set_xalign(GTK_LABEL(tmp), 0.0);
+       gtk_grid_attach(GTK_GRID(grid), tmp, 0, 1, 1, 1);
+
+       frdp->with = gtk_entry_new();
+       gtk_widget_set_hexpand(frdp->with, TRUE);
+       gtk_grid_attach(GTK_GRID(grid), frdp->with, 1, 1, 1, 1);
+    }
+
+    // Checkboxes
+    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
+    gtk_box_append(GTK_BOX(vbox), hbox);
+
+    frdp->wword = gtk_check_button_new_with_label(_("Match whole word only"));
+    gtk_check_button_set_active(GTK_CHECK_BUTTON(frdp->wword),
+           (gboolean)wword);
+    gtk_box_append(GTK_BOX(hbox), frdp->wword);
+
+    frdp->mcase = gtk_check_button_new_with_label(_("Match case"));
+    gtk_check_button_set_active(GTK_CHECK_BUTTON(frdp->mcase),
+           (gboolean)mcase);
+    gtk_box_append(GTK_BOX(hbox), frdp->mcase);
+
+    // Direction radio buttons
+    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
+    gtk_box_append(GTK_BOX(vbox), hbox);
+
+    tmp = gtk_label_new(_("Direction:"));
+    gtk_box_append(GTK_BOX(hbox), tmp);
+
+    frdp->up = gtk_check_button_new_with_label(_("Up"));
+    gtk_box_append(GTK_BOX(hbox), frdp->up);
+
+    frdp->down = gtk_check_button_new_with_label(_("Down"));
+    gtk_check_button_set_group(GTK_CHECK_BUTTON(frdp->down),
+           GTK_CHECK_BUTTON(frdp->up));
+    gtk_check_button_set_active(GTK_CHECK_BUTTON(frdp->down), TRUE);
+    gtk_box_append(GTK_BOX(hbox), frdp->down);
+
+    // Action buttons
+    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+    gtk_widget_set_halign(hbox, GTK_ALIGN_END);
+    gtk_box_append(GTK_BOX(vbox), hbox);
+
+    btn = gtk_button_new_with_label(_("Find Next"));
+    gtk_widget_set_sensitive(btn, sensitive);
+    g_signal_connect(btn, "clicked", G_CALLBACK(find_replace_cb),
+           GINT_TO_POINTER(do_replace ? FRD_R_FINDNEXT : FRD_FINDNEXT));
+    gtk_box_append(GTK_BOX(hbox), btn);
+
+    if (do_replace)
+    {
+       btn = gtk_button_new_with_label(_("Replace"));
+       g_signal_connect(btn, "clicked", G_CALLBACK(find_replace_cb),
+               GINT_TO_POINTER(FRD_REPLACE));
+       gtk_box_append(GTK_BOX(hbox), btn);
+
+       btn = gtk_button_new_with_label(_("Replace All"));
+       g_signal_connect(btn, "clicked", G_CALLBACK(find_replace_cb),
+               GINT_TO_POINTER(FRD_REPLACEALL));
+       gtk_box_append(GTK_BOX(hbox), btn);
+    }
+
+    btn = gtk_button_new_with_label(_("Close"));
+    g_signal_connect_swapped(btn, "clicked",
+           G_CALLBACK(gtk_window_destroy), frdp->dialog);
+    gtk_box_append(GTK_BOX(hbox), btn);
+
+    // Connect Enter key in entry to Find Next
+    g_signal_connect_swapped(frdp->what, "activate",
+           G_CALLBACK(find_replace_cb),
+           GINT_TO_POINTER(do_replace ? FRD_R_FINDNEXT : FRD_FINDNEXT));
+
+    gtk_window_present(GTK_WINDOW(frdp->dialog));
+    gtk_widget_grab_focus(frdp->what);
+    if (do_replace && entry_text != NULL && entry_text[0] != NUL)
+       gtk_widget_grab_focus(frdp->with);
+
+    vim_free(entry_text);
+}
+
+    void
+gui_mch_find_dialog(exarg_T *eap)
+{
+    if (gui.in_use)
+       find_replace_dialog_create(eap->arg, FALSE);
+}
+
+    void
+gui_mch_replace_dialog(exarg_T *eap)
+{
+    if (gui.in_use)
+       find_replace_dialog_create(eap->arg, TRUE);
+}
+
+/*
+ * ============================================================
+ * Help find (for :helpfind command)
+ * ============================================================
+ */
+
+    void
+ex_helpfind(exarg_T *eap UNUSED)
+{
+    do_cmdline_cmd((char_u *)"emenu ToolBar.FindHelp");
+}
+
+
+/*
+ * ============================================================
+ * Printing with GtkPrintOperation
+ * ============================================================
+ */
+#ifdef FEAT_GUI_GTK_PRINT
+
+typedef struct
+{
+    linenr_T   first_line;     // first line to print (from range)
+    linenr_T   last_line;      // last line to print (from range)
+    int                n_pages;        // total number of pages
+    int                lines_per_page; // lines that fit on one page
+    PangoFontDescription *font_desc;
+    int                do_syntax;      // whether to use syntax highlighting
+    double     line_height;    // line height in points
+    double     char_width;     // character width in points
+} print_data_T;
+
+/*
+ * "begin-print" signal handler.
+ * Calculate pagination based on page size and font metrics.
+ */
+    static void
+print_begin_cb(
+    GtkPrintOperation  *op,
+    GtkPrintContext    *context,
+    gpointer           user_data)
+{
+    print_data_T    *pd = (print_data_T *)user_data;
+    PangoLayout            *layout;
+    PangoFontMetrics *metrics;
+    double         page_height;
+    int                    total_lines;
+
+    page_height = gtk_print_context_get_height(context);
+
+    // Create a PangoLayout to measure font metrics on the print surface.
+    layout = gtk_print_context_create_pango_layout(context);
+    pango_layout_set_font_description(layout, pd->font_desc);
+
+    metrics = pango_context_get_metrics(
+           pango_layout_get_context(layout),
+           pd->font_desc, NULL);
+
+    pd->line_height = (double)(pango_font_metrics_get_ascent(metrics)
+           + pango_font_metrics_get_descent(metrics)) / PANGO_SCALE;
+    pd->char_width = (double)pango_font_metrics_get_approximate_char_width(
+           metrics) / PANGO_SCALE;
+
+    pango_font_metrics_unref(metrics);
+    g_object_unref(layout);
+
+    if (pd->line_height <= 0)
+       pd->line_height = 12.0;
+
+    pd->lines_per_page = (int)(page_height / pd->line_height);
+    if (pd->lines_per_page <= 0)
+       pd->lines_per_page = 1;
+
+    total_lines = (int)(pd->last_line - pd->first_line + 1);
+    pd->n_pages = (total_lines + pd->lines_per_page - 1) / pd->lines_per_page;
+    if (pd->n_pages <= 0)
+       pd->n_pages = 1;
+
+    gtk_print_operation_set_n_pages(op, pd->n_pages);
+}
+
+/*
+ * "draw-page" signal handler.
+ * Render one page of buffer text with optional syntax highlighting.
+ */
+    static void
+print_draw_page_cb(
+    GtkPrintOperation  *op UNUSED,
+    GtkPrintContext    *context,
+    int                        page_nr,
+    gpointer           user_data)
+{
+    print_data_T    *pd = (print_data_T *)user_data;
+    cairo_t        *cr;
+    linenr_T       lnum;
+    linenr_T       first;
+    linenr_T       last;
+    int                    page_line;
+    double         y;
+
+    cr = gtk_print_context_get_cairo_context(context);
+
+    first = pd->first_line + (linenr_T)page_nr * pd->lines_per_page;
+    last = first + pd->lines_per_page - 1;
+    if (last > pd->last_line)
+       last = pd->last_line;
+
+    y = 0;
+    page_line = 0;
+
+    for (lnum = first; lnum <= last; ++lnum, ++page_line)
+    {
+       char_u          *line;
+       PangoLayout     *layout;
+       PangoAttrList   *attr_list;
+
+       line = ml_get(lnum);
+       layout = gtk_print_context_create_pango_layout(context);
+       pango_layout_set_font_description(layout, pd->font_desc);
+
+       attr_list = pango_attr_list_new();
+
+# ifdef FEAT_SYN_HL
+       if (pd->do_syntax && syntax_present(curwin))
+       {
+           colnr_T     col;
+           int         prev_syn_id = -1;
+           int         attr_start = 0;
+           long_u      prev_fg = 0;
+           int         prev_bold = FALSE;
+           int         prev_italic = FALSE;
+           int         len = (int)STRLEN(line);
+
+           for (col = 0; col < len; )
+           {
+               int         id;
+               int         outputlen;
+               long_u      fg_color;
+               int         is_bold;
+               int         is_italic;
+
+               if (has_mbyte)
+               {
+                   outputlen = (*mb_ptr2len)(line + col);
+                   if (outputlen < 1)
+                       outputlen = 1;
+               }
+               else
+                   outputlen = 1;
+
+               id = syn_get_id(curwin, lnum, col, 1, NULL, FALSE);
+               if (id > 0)
+                   id = syn_get_final_id(id);
+               else
+                   id = 0;
+               // syn_get_id may invalidate the line pointer.
+               line = ml_get(lnum);
+
+               fg_color = highlight_gui_color_rgb(id, TRUE);
+               is_bold = (highlight_has_attr(id, HL_BOLD, 'g') != NULL);
+               is_italic = (highlight_has_attr(id, HL_ITALIC, 'g') != NULL);
+
+               // When attributes change, flush the previous run.
+               if (id != prev_syn_id && col > 0)
+               {
+                   if (prev_fg != 0 && prev_fg != (long_u)0xffffffL)
+                   {
+                       PangoAttribute *a = pango_attr_foreground_new(
+                               (guint16)(((prev_fg >> 16) & 0xff) * 257),
+                               (guint16)(((prev_fg >> 8) & 0xff) * 257),
+                               (guint16)((prev_fg & 0xff) * 257));
+                       a->start_index = attr_start;
+                       a->end_index = col;
+                       pango_attr_list_insert(attr_list, a);
+                   }
+                   if (prev_bold)
+                   {
+                       PangoAttribute *a = pango_attr_weight_new(
+                               PANGO_WEIGHT_BOLD);
+                       a->start_index = attr_start;
+                       a->end_index = col;
+                       pango_attr_list_insert(attr_list, a);
+                   }
+                   if (prev_italic)
+                   {
+                       PangoAttribute *a = pango_attr_style_new(
+                               PANGO_STYLE_ITALIC);
+                       a->start_index = attr_start;
+                       a->end_index = col;
+                       pango_attr_list_insert(attr_list, a);
+                   }
+                   attr_start = col;
+               }
+
+               prev_syn_id = id;
+               prev_fg = fg_color;
+               prev_bold = is_bold;
+               prev_italic = is_italic;
+
+               col += outputlen;
+           }
+
+           // Flush the last run.
+           if (attr_start < len)
+           {
+               if (prev_fg != 0 && prev_fg != (long_u)0xffffffL)
+               {
+                   PangoAttribute *a = pango_attr_foreground_new(
+                           (guint16)(((prev_fg >> 16) & 0xff) * 257),
+                           (guint16)(((prev_fg >> 8) & 0xff) * 257),
+                           (guint16)((prev_fg & 0xff) * 257));
+                   a->start_index = attr_start;
+                   a->end_index = len;
+                   pango_attr_list_insert(attr_list, a);
+               }
+               if (prev_bold)
+               {
+                   PangoAttribute *a = pango_attr_weight_new(
+                           PANGO_WEIGHT_BOLD);
+                   a->start_index = attr_start;
+                   a->end_index = len;
+                   pango_attr_list_insert(attr_list, a);
+               }
+               if (prev_italic)
+               {
+                   PangoAttribute *a = pango_attr_style_new(
+                           PANGO_STYLE_ITALIC);
+                   a->start_index = attr_start;
+                   a->end_index = len;
+                   pango_attr_list_insert(attr_list, a);
+               }
+           }
+       }
+# endif // FEAT_SYN_HL
+
+       pango_layout_set_attributes(layout, attr_list);
+
+       // Expand tabs. Use a tab array matching Vim's tabstop.
+       {
+           PangoTabArray *tabs;
+           int tab_width = (int)(curbuf->b_p_ts * pd->char_width);
+
+           if (tab_width <= 0)
+               tab_width = (int)(8 * pd->char_width);
+           tabs = pango_tab_array_new(1, TRUE);
+           pango_tab_array_set_tab(tabs, 0, PANGO_TAB_LEFT, tab_width);
+           pango_layout_set_tabs(layout, tabs);
+           pango_tab_array_free(tabs);
+       }
+
+       pango_layout_set_text(layout, (const char *)line, -1);
+
+       cairo_move_to(cr, 0, y);
+       pango_cairo_show_layout(cr, layout);
+
+       pango_attr_list_unref(attr_list);
+       g_object_unref(layout);
+
+       y += pd->line_height;
+    }
+}
+
+/*
+ * Main entry point for GTK4 native printing.
+ * Called from ex_hardcopy() when running in a GTK4 GUI.
+ */
+    void
+gui_gtk4_hardcopy(exarg_T *eap)
+{
+    GtkPrintOperation  *op;
+    GtkPrintOperationResult res;
+    print_data_T       pd;
+    char_u             *font_name;
+
+    static GtkPrintSettings *settings = NULL;
+
+    CLEAR_FIELD(pd);
+    pd.first_line = eap->line1;
+    pd.last_line = eap->line2;
+
+    // Use 'printfont' if set, otherwise fall back to 'guifont'.
+    font_name = *p_pfn != NUL ? p_pfn : p_guifont;
+    if (font_name == NULL || *font_name == NUL)
+       font_name = (char_u *)"Monospace 10";
+
+    pd.font_desc = pango_font_description_from_string((const char *)font_name);
+    if (pd.font_desc == NULL)
+    {
+       semsg(_(e_unknown_font_str), font_name);
+       return;
+    }
+
+    // Ensure the font description has a size (default 10pt if missing).
+    if (pango_font_description_get_size(pd.font_desc) == 0)
+       pango_font_description_set_size(pd.font_desc, 10 * PANGO_SCALE);
+
+# ifdef FEAT_SYN_HL
+    pd.do_syntax = syntax_present(curwin);
+# endif
+
+    op = gtk_print_operation_new();
+
+    if (settings != NULL)
+       gtk_print_operation_set_print_settings(op, settings);
+
+    gtk_print_operation_set_job_name(op,
+           curbuf->b_fname != NULL
+               ? (const char *)curbuf->b_fname : "Vim");
+    gtk_print_operation_set_show_progress(op, TRUE);
+    gtk_print_operation_set_unit(op, GTK_UNIT_POINTS);
+
+    g_signal_connect(op, "begin-print", G_CALLBACK(print_begin_cb), &pd);
+    g_signal_connect(op, "draw-page", G_CALLBACK(print_draw_page_cb), &pd);
+
+    res = gtk_print_operation_run(op,
+           GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG,
+           GTK_WINDOW(gui.mainwin), NULL);
+
+    if (res == GTK_PRINT_OPERATION_RESULT_APPLY)
+    {
+       if (settings != NULL)
+           g_object_unref(settings);
+       settings = g_object_ref(
+               gtk_print_operation_get_print_settings(op));
+    }
+
+    g_object_unref(op);
+    pango_font_description_free(pd.font_desc);
+}
+
+#endif // FEAT_GUI_GTK_PRINT
+
+#endif // FEAT_GUI_GTK
diff --git a/src/gui_gtk4_f.c b/src/gui_gtk4_f.c
new file mode 100644 (file)
index 0000000..759f106
--- /dev/null
@@ -0,0 +1,324 @@
+/* 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.
+ *
+ * GTK4 GtkForm widget - a simple container for absolute child positioning.
+ * This is a clean rewrite of gui_gtk_f.c for GTK4.
+ *
+ * In GTK4, widgets no longer have their own GdkWindows (now GdkSurface),
+ * GtkContainer is removed, and child positioning uses GskTransform via
+ * gtk_widget_allocate().  This makes the form widget much simpler.
+ */
+
+#include "vim.h"
+#include <gtk/gtk.h>
+#include "gui_gtk4_f.h"
+
+typedef struct _GtkFormChild GtkFormChild;
+
+struct _GtkFormChild
+{
+    GtkWidget *widget;
+    gint x;
+    gint y;
+};
+
+// Forward declarations
+static void gui_gtk_form_class_init(GtkFormClass *klass);
+static void gui_gtk_form_init(GtkForm *form);
+static void form_measure(GtkWidget *widget, GtkOrientation orientation,
+       int for_size, int *minimum, int *natural,
+       int *minimum_baseline, int *natural_baseline);
+static void form_size_allocate(GtkWidget *widget, int width, int height,
+       int baseline);
+static void form_snapshot(GtkWidget *widget, GtkSnapshot *snapshot);
+static void form_dispose(GObject *object);
+static void form_position_child(GtkForm *form, GtkFormChild *child,
+       gboolean force_allocate);
+
+G_DEFINE_TYPE(GtkForm, gui_gtk_form, GTK_TYPE_WIDGET)
+
+// Public interface
+
+    GtkWidget *
+gui_gtk_form_new(void)
+{
+    return GTK_WIDGET(g_object_new(GTK_TYPE_FORM, NULL));
+}
+
+    void
+gui_gtk_form_put(
+       GtkForm     *form,
+       GtkWidget   *child_widget,
+       gint        x,
+       gint        y)
+{
+    GtkFormChild *child;
+
+    g_return_if_fail(GTK_IS_FORM(form));
+
+    child = g_new(GtkFormChild, 1);
+    if (child == NULL)
+       return;
+
+    child->widget = child_widget;
+    child->x = x;
+    child->y = y;
+
+    gtk_widget_set_size_request(child->widget, -1, -1);
+
+    form->children = g_list_append(form->children, child);
+
+    gtk_widget_set_parent(child_widget, GTK_WIDGET(form));
+    form_position_child(form, child, TRUE);
+}
+
+    void
+gui_gtk_form_move(
+       GtkForm     *form,
+       GtkWidget   *child_widget,
+       gint        x,
+       gint        y)
+{
+    GList *tmp_list;
+
+    g_return_if_fail(GTK_IS_FORM(form));
+
+    for (tmp_list = form->children; tmp_list; tmp_list = tmp_list->next)
+    {
+       GtkFormChild *child = tmp_list->data;
+       if (child->widget == child_widget)
+       {
+           child->x = x;
+           child->y = y;
+           form_position_child(form, child, TRUE);
+           return;
+       }
+    }
+}
+
+    void
+gui_gtk_form_move_resize(
+       GtkForm     *form,
+       GtkWidget   *widget,
+       gint        x,
+       gint        y,
+       gint        w,
+       gint        h)
+{
+    gtk_widget_set_size_request(widget, w, h);
+    gui_gtk_form_move(form, widget, x, y);
+}
+
+    void
+gui_gtk_form_remove(GtkForm *form, GtkWidget *child_widget)
+{
+    GList *tmp_list;
+
+    g_return_if_fail(GTK_IS_FORM(form));
+
+    for (tmp_list = form->children; tmp_list; tmp_list = tmp_list->next)
+    {
+       GtkFormChild *child = tmp_list->data;
+       if (child->widget == child_widget)
+       {
+           form->children = g_list_remove_link(form->children, tmp_list);
+           g_list_free_1(tmp_list);
+           gtk_widget_unparent(child_widget);
+           g_free(child);
+           return;
+       }
+    }
+}
+
+    void
+gui_gtk_form_freeze(GtkForm *form)
+{
+    g_return_if_fail(GTK_IS_FORM(form));
+    ++form->freeze_count;
+}
+
+    void
+gui_gtk_form_thaw(GtkForm *form)
+{
+    g_return_if_fail(GTK_IS_FORM(form));
+
+    if (!form->freeze_count)
+       return;
+
+    if (!(--form->freeze_count))
+    {
+       GList *tmp_list;
+
+       for (tmp_list = form->children; tmp_list; tmp_list = tmp_list->next)
+           form_position_child(form, tmp_list->data, FALSE);
+       gtk_widget_queue_draw(GTK_WIDGET(form));
+    }
+}
+
+// GObject/GtkWidget class implementation
+
+    static void
+gui_gtk_form_class_init(GtkFormClass *klass)
+{
+    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+
+    gobject_class->dispose = form_dispose;
+
+    widget_class->measure = form_measure;
+    widget_class->size_allocate = form_size_allocate;
+    widget_class->snapshot = form_snapshot;
+}
+
+    static void
+gui_gtk_form_init(GtkForm *form)
+{
+    form->children = NULL;
+    form->freeze_count = 0;
+}
+
+    static void
+form_measure(
+       GtkWidget       *widget UNUSED,
+       GtkOrientation  orientation UNUSED,
+       int             for_size UNUSED,
+       int             *minimum,
+       int             *natural,
+       int             *minimum_baseline,
+       int             *natural_baseline)
+{
+    *minimum = 1;
+    *natural = 1;
+    *minimum_baseline = -1;
+    *natural_baseline = -1;
+}
+
+static guint form_resize_idle_id = 0;
+static int form_last_width = 0;
+static int form_last_height = 0;
+
+    static gboolean
+form_resize_idle_cb(gpointer data UNUSED)
+{
+    int w, h;
+
+    form_resize_idle_id = 0;
+
+    // Use drawarea's actual allocation, not formwin's
+    if (gui.drawarea == NULL)
+       return FALSE;
+    w = gtk_widget_get_width(gui.drawarea);
+    h = gtk_widget_get_height(gui.drawarea);
+
+    if (w > 1 && h > 1)
+       gui_resize_shell(w, h);
+
+    return FALSE;
+}
+
+    static void
+form_size_allocate(GtkWidget *widget, int width, int height,
+       int baseline UNUSED)
+{
+    GtkForm *form;
+    GList *tmp_list;
+
+    g_return_if_fail(GTK_IS_FORM(widget));
+
+    form = GTK_FORM(widget);
+
+    for (tmp_list = form->children; tmp_list; tmp_list = tmp_list->next)
+       form_position_child(form, tmp_list->data, TRUE);
+
+    // Notify Vim about size change via idle callback
+    if (width != form_last_width || height != form_last_height)
+    {
+       form_last_width = width;
+       form_last_height = height;
+       if (form_resize_idle_id == 0)
+           form_resize_idle_id = g_idle_add(form_resize_idle_cb, NULL);
+    }
+}
+
+    static void
+form_snapshot(GtkWidget *widget, GtkSnapshot *snapshot)
+{
+    GtkForm *form;
+    GList *tmp_list;
+
+    g_return_if_fail(GTK_IS_FORM(widget));
+
+    form = GTK_FORM(widget);
+
+    for (tmp_list = form->children; tmp_list; tmp_list = tmp_list->next)
+    {
+       GtkFormChild *child = tmp_list->data;
+       if (child->widget != NULL
+               && GTK_IS_WIDGET(child->widget)
+               && gtk_widget_get_parent(child->widget) == widget)
+           gtk_widget_snapshot_child(widget, child->widget, snapshot);
+    }
+}
+
+    static void
+form_dispose(GObject *object)
+{
+    GtkForm *form = GTK_FORM(object);
+    GList *tmp_list;
+
+    tmp_list = form->children;
+    while (tmp_list)
+    {
+       GtkFormChild *child = tmp_list->data;
+       tmp_list = tmp_list->next;
+
+       gtk_widget_unparent(child->widget);
+       g_free(child);
+    }
+    g_list_free(form->children);
+    form->children = NULL;
+
+    G_OBJECT_CLASS(gui_gtk_form_parent_class)->dispose(object);
+}
+
+// Child positioning using GskTransform
+
+    static void
+form_position_child(
+       GtkForm         *form UNUSED,
+       GtkFormChild    *child,
+       gboolean        force_allocate)
+{
+    if (!force_allocate)
+       return;
+
+    if (child->widget == NULL || !GTK_IS_WIDGET(child->widget))
+       return;
+
+    {
+       GtkRequisition requisition;
+       GskTransform *transform;
+       int w, h;
+
+       gtk_widget_get_preferred_size(child->widget, &requisition, NULL);
+       w = requisition.width;
+       h = requisition.height;
+
+       // If widget has no size request (e.g. drawarea), use parent size
+       if (w <= 0)
+           w = gtk_widget_get_width(GTK_WIDGET(form));
+       if (h <= 0)
+           h = gtk_widget_get_height(GTK_WIDGET(form));
+       if (w <= 0) w = 1;
+       if (h <= 0) h = 1;
+
+       transform = gsk_transform_translate(NULL,
+               &GRAPHENE_POINT_INIT((float)child->x, (float)child->y));
+       gtk_widget_allocate(child->widget, w, h, -1, transform);
+    }
+}
diff --git a/src/gui_gtk4_f.h b/src/gui_gtk4_f.h
new file mode 100644 (file)
index 0000000..a927061
--- /dev/null
@@ -0,0 +1,59 @@
+/* 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.
+ *
+ * GTK4 GtkForm widget - a simple container for absolute positioning.
+ */
+
+#ifndef GUI_GTK4_FORM_H
+#define GUI_GTK4_FORM_H
+
+#include <gtk/gtk.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define GTK_TYPE_FORM          (gui_gtk_form_get_type())
+#define GTK_FORM(obj)          (G_TYPE_CHECK_INSTANCE_CAST((obj), GTK_TYPE_FORM, GtkForm))
+#define GTK_FORM_CLASS(klass)  (G_TYPE_CHECK_CLASS_CAST((klass), GTK_TYPE_FORM, GtkFormClass))
+#define GTK_IS_FORM(obj)       (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTK_TYPE_FORM))
+
+typedef struct _GtkForm GtkForm;
+typedef struct _GtkFormClass GtkFormClass;
+
+struct _GtkForm
+{
+    GtkWidget widget;
+    GList *children;
+    gint freeze_count;
+};
+
+struct _GtkFormClass
+{
+    GtkWidgetClass parent_class;
+};
+
+GType gui_gtk_form_get_type(void);
+
+GtkWidget *gui_gtk_form_new(void);
+
+void gui_gtk_form_put(GtkForm *form, GtkWidget *widget, gint x, gint y);
+
+void gui_gtk_form_move(GtkForm *form, GtkWidget *widget, gint x, gint y);
+
+void gui_gtk_form_move_resize(GtkForm *form, GtkWidget *widget,
+       gint x, gint y, gint w, gint h);
+
+void gui_gtk_form_remove(GtkForm *form, GtkWidget *widget);
+
+void gui_gtk_form_freeze(GtkForm *form);
+void gui_gtk_form_thaw(GtkForm *form);
+
+#ifdef __cplusplus
+}
+#endif
+#endif // GUI_GTK4_FORM_H
index 26b125d919402dc920b780de741c77ea1729fb2f..acc6351936075557c033a57abf2107d4ad129be9 100644 (file)
 #endif
 
 #if defined(FEAT_GUI_GTK) && defined(FEAT_XIM)
-# if GTK_CHECK_VERSION(3,0,0)
+# ifdef USE_GTK4
+#  include <gdk/gdkkeysyms.h>
+# elif GTK_CHECK_VERSION(3,0,0)
 #  include <gdk/gdkkeysyms-compat.h>
 # else
 #  include <gdk/gdkkeysyms.h>
 # endif
-# ifdef MSWIN
-#  include <gdk/gdkwin32.h>
-# else
-#  include <gdk/gdkx.h>
+# if !defined(USE_GTK4)
+#  ifdef MSWIN
+#   include <gdk/gdkwin32.h>
+#  else
+#   include <gdk/gdkx.h>
+#  endif
 # endif
 #endif
 
@@ -185,7 +189,11 @@ static int im_preedit_cursor   = 0;        // cursor offset in characters
 static int im_preedit_trailing = 0;    // number of characters after cursor
 
 static unsigned long im_commit_handler_id  = 0;
+#  ifdef USE_GTK4
+static unsigned int  im_activatekey_keyval = GDK_KEY_VoidSymbol;
+#  else
 static unsigned int  im_activatekey_keyval = GDK_VoidSymbol;
+#  endif
 static unsigned int  im_activatekey_state  = 0;
 
 static GtkWidget *preedit_window = NULL;
@@ -272,6 +280,19 @@ im_preedit_window_set_position(void)
     if (preedit_window == NULL)
        return;
 
+#  ifdef USE_GTK4
+    // GTK4: positioning popup windows is limited.
+    // Use a simpler approach - just place near the cursor.
+    x = FILL_X(gui.col);
+    y = FILL_Y(gui.row) + gui.char_height;
+    width = 0;
+    height = 0;
+    screen_x = 0;
+    screen_y = 0;
+    screen_width = 0;
+    screen_height = 0;
+    // GTK4 doesn't have gtk_window_move; preedit is shown in-place.
+#  else
     gui_gtk_get_screen_geom_of_win(gui.drawarea, 0, 0,
                          &screen_x, &screen_y, &screen_width, &screen_height);
     gdk_window_get_origin(gtk_widget_get_window(gui.drawarea), &x, &y);
@@ -283,6 +304,7 @@ im_preedit_window_set_position(void)
     if (y + height > screen_y + screen_height)
        y = screen_y + screen_height - height;
     gtk_window_move(GTK_WINDOW(preedit_window), x, y);
+#  endif
 }
 
     static void
@@ -305,18 +327,28 @@ im_preedit_window_open(void)
 
     if (preedit_window == NULL)
     {
+#  ifdef USE_GTK4
+       preedit_window = gtk_window_new();
+#  else
        preedit_window = gtk_window_new(GTK_WINDOW_POPUP);
+#  endif
        gtk_window_set_transient_for(GTK_WINDOW(preedit_window),
                                                     GTK_WINDOW(gui.mainwin));
        preedit_label = gtk_label_new("");
        gtk_widget_set_name(preedit_label, "vim-gui-preedit-area");
+#  ifdef USE_GTK4
+       gtk_window_set_child(GTK_WINDOW(preedit_window), preedit_label);
+#  else
        gtk_container_add(GTK_CONTAINER(preedit_window), preedit_label);
+#  endif
     }
 
 #  if GTK_CHECK_VERSION(3,16,0)
     {
+#   ifndef USE_GTK4
        GtkStyleContext * const context
                                  = gtk_widget_get_style_context(preedit_label);
+#   endif
        GtkCssProvider * const provider = gtk_css_provider_new();
        gchar              *css = NULL;
        const char * const fontname
@@ -329,10 +361,15 @@ im_preedit_window_open(void)
        {
            // fontsize was given in points.  Convert it into that in pixels
            // to use with CSS.
+#   ifdef USE_GTK4
+           // GTK4: assume 96 DPI as default
+           fontsize = 96 * fontsize / 72;
+#   else
            GdkScreen * const screen
                  = gdk_window_get_screen(gtk_widget_get_window(gui.mainwin));
            const gdouble dpi = gdk_screen_get_resolution(screen);
            fontsize = dpi * fontsize / 72;
+#   endif
        }
        if (fontsize > 0)
            fontsize_propval = g_strdup_printf("%dpx", fontsize);
@@ -355,9 +392,16 @@ im_preedit_window_open(void)
                (gui.back_pixel >> 8) & 0xff,
                gui.back_pixel & 0xff);
 
+#   ifdef USE_GTK4
+       gtk_css_provider_load_from_string(provider, css);
+       gtk_style_context_add_provider_for_display(
+               gdk_display_get_default(),
+               GTK_STYLE_PROVIDER(provider), G_MAXUINT);
+#   else
        gtk_css_provider_load_from_data(provider, css, -1, NULL);
        gtk_style_context_add_provider(context,
                                     GTK_STYLE_PROVIDER(provider), G_MAXUINT);
+#   endif
 
        g_free(css);
        g_free(fontsize_propval);
@@ -396,9 +440,13 @@ im_preedit_window_open(void)
        layout = gtk_label_get_layout(GTK_LABEL(preedit_label));
        pango_layout_get_pixel_size(layout, &w, &h);
        h = MAX(h, gui.char_height);
+#  ifdef USE_GTK4
+       gtk_window_set_default_size(GTK_WINDOW(preedit_window), w, h);
+       gtk_widget_set_visible(preedit_window, TRUE);
+#  else
        gtk_window_resize(GTK_WINDOW(preedit_window), w, h);
-
        gtk_widget_show_all(preedit_window);
+#  endif
 
        im_preedit_window_set_position();
     }
@@ -411,7 +459,11 @@ im_preedit_window_open(void)
 im_preedit_window_close(void)
 {
     if (preedit_window != NULL)
+#  ifdef USE_GTK4
+       gtk_widget_set_visible(preedit_window, FALSE);
+#  else
        gtk_widget_hide(preedit_window);
+#  endif
 }
 
     static void
@@ -874,7 +926,9 @@ xim_init(void)
 #  endif
 
     g_return_if_fail(gui.drawarea != NULL);
+#  ifndef USE_GTK4
     g_return_if_fail(gtk_widget_get_window(gui.drawarea) != NULL);
+#  endif
 
     xic = gtk_im_multicontext_new();
     g_object_ref(xic);
@@ -888,7 +942,11 @@ xim_init(void)
     g_signal_connect(G_OBJECT(xic), "preedit_end",
                     G_CALLBACK(&im_preedit_end_cb), NULL);
 
+#  ifdef USE_GTK4
+    gtk_im_context_set_client_widget(xic, gui.drawarea);
+#  else
     gtk_im_context_set_client_window(xic, gtk_widget_get_window(gui.drawarea));
+#  endif
 }
 
     void
@@ -911,6 +969,7 @@ im_shutdown(void)
     xim_has_preediting = FALSE;
 }
 
+#  ifndef USE_GTK4
 /*
  * Convert the string argument to keyval and state for GdkEventKey.
  * If str is valid return TRUE, otherwise FALSE.
@@ -980,7 +1039,9 @@ im_xim_isvalid_imactivate(void)
                               &im_activatekey_keyval,
                               &im_activatekey_state);
 }
+#  endif // !USE_GTK4
 
+#  ifndef USE_GTK4
     static void
 im_synthesize_keypress(unsigned int keyval, unsigned int state)
 {
@@ -1008,6 +1069,7 @@ im_synthesize_keypress(unsigned int keyval, unsigned int state)
 
     gdk_event_free((GdkEvent *)event);
 }
+#  endif // !USE_GTK4
 
     void
 xim_reset(void)
@@ -1027,6 +1089,11 @@ xim_reset(void)
        {
            xim_set_focus(gui.in_focus);
 
+#  ifdef USE_GTK4
+           im_shutdown();
+           xim_init();
+           xim_set_focus(gui.in_focus);
+#  else
            if (im_activatekey_keyval != GDK_VoidSymbol)
            {
                if (im_is_active)
@@ -1043,6 +1110,7 @@ xim_reset(void)
                xim_init();
                xim_set_focus(gui.in_focus);
            }
+#  endif
        }
     }
 
@@ -1051,12 +1119,13 @@ xim_reset(void)
     xim_has_preediting = FALSE;
 }
 
+#  ifndef USE_GTK4
     int
 xim_queue_key_press_event(GdkEventKey *event, int down)
 {
-#  ifdef FEAT_GUI_GTK
+#   ifdef FEAT_GUI_GTK
     if (event->state & GDK_SUPER_MASK) return FALSE;
-#  endif
+#   endif
     if (down)
     {
        // Workaround GTK2 XIM 'feature' that always converts keypad keys to
@@ -1185,6 +1254,23 @@ xim_queue_key_press_event(GdkEventKey *event, int down)
 
     return FALSE;
 }
+#  else // USE_GTK4
+// GTK4: imactivatekey is not supported because GTK4's GtkIMContext
+// does not allow synthesizing key events for IM activation.
+    int
+im_xim_isvalid_imactivate(void)
+{
+    // Empty string is always valid (means no activation key).
+    // Any other value is not supported in GTK4.
+    return p_imak[0] == NUL;
+}
+
+    int
+xim_queue_key_press_event(GdkEvent *event UNUSED, int down UNUSED)
+{
+    return FALSE;
+}
+#  endif // !USE_GTK4
 
     int
 im_get_status(void)
index e87caf3020907238e5c06cb81ab49d706f7e7481..2eb63ea5889388bb5ef8a7edeff41f04ef1599e5 100644 (file)
@@ -560,6 +560,16 @@ ex_hardcopy(exarg_T *eap)
     CLEAR_FIELD(settings);
     settings.has_color = TRUE;
 
+#ifdef FEAT_GUI_GTK_PRINT
+    // Use the native GTK print dialog only for interactive printing;
+    // ":hardcopy >file" must fall through to the PostScript writer.
+    if (gui.in_use && *eap->arg != '>')
+    {
+       gui_gtk4_hardcopy(eap);
+       return;
+    }
+#endif
+
 #ifdef FEAT_POSTSCRIPT
     if (*eap->arg == '>')
     {
index f1369d68ae25ec3221f0d17376be18a7a5088230..2057c23a92faa1e1ca3ba4ae63466990e084c1a8 100644 (file)
@@ -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-17 19:50+0000\n"
+"POT-Creation-Date: 2026-05-19 18:20+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -921,9 +921,11 @@ msgstr ""
 msgid "VIM - Search..."
 msgstr ""
 
+#. "Find what:" label + entry
 msgid "Find what:"
 msgstr ""
 
+#. "Replace with:" label + entry
 msgid "Replace with:"
 msgstr ""
 
@@ -957,6 +959,12 @@ msgstr ""
 msgid "_Close"
 msgstr ""
 
+msgid "Direction:"
+msgstr ""
+
+msgid "Close"
+msgstr ""
+
 msgid "Vim: Received \"die\" request from session manager\n"
 msgstr ""
 
@@ -3454,6 +3462,9 @@ msgstr ""
 msgid "without GUI."
 msgstr ""
 
+msgid "with GTK4 GUI."
+msgstr ""
+
 msgid "with GTK3 GUI."
 msgstr ""
 
index e46b25624f2acf20e2a7cc5de27532b3a3648317..ed7a5a054e990b618fa96cf982f409fcf49b0769 100644 (file)
@@ -302,8 +302,12 @@ extern char_u *vimpty_getenv(const char_u *string);        // in misc2.c
 #   include "gui_w32.pro"
 #  endif
 #  ifdef FEAT_GUI_GTK
-#   include "gui_gtk.pro"
-#   include "gui_gtk_x11.pro"
+#   ifdef USE_GTK4
+#    include "gui_gtk4.pro"
+#   else
+#    include "gui_gtk.pro"
+#    include "gui_gtk_x11.pro"
+#   endif
 #  endif
 #  ifdef FEAT_GUI_MOTIF
 #   include "gui_motif.pro"
diff --git a/src/proto/gui_gtk4.pro b/src/proto/gui_gtk4.pro
new file mode 100644 (file)
index 0000000..1f13ab6
--- /dev/null
@@ -0,0 +1,110 @@
+/* gui_gtk4.c */
+void gui_mch_prepare(int *argc, char **argv);
+void gui_mch_free_all(void);
+int gui_mch_is_blinking(void);
+int gui_mch_is_blink_off(void);
+void gui_mch_set_blinking(long waittime, long on, long off);
+void gui_mch_stop_blink(int may_call_gui_update_cursor);
+void gui_mch_start_blink(void);
+int gui_mch_early_init_check(int give_message);
+int gui_mch_init_check(void);
+int gui_mch_init(void);
+void gui_mch_new_colors(void);
+int gui_mch_open(void);
+void gui_mch_exit(int rc);
+int gui_mch_get_winpos(int *x, int *y);
+void gui_mch_set_winpos(int x, int y);
+int gui_mch_maximized(void);
+void gui_mch_unmaximize(void);
+void gui_mch_newfont(void);
+void gui_mch_settitle(char_u *title, char_u *icon);
+void gui_mch_set_shellsize(int width, int height, int min_width, int min_height, int base_width, int base_height, int direction);
+void gui_mch_get_screen_dimensions(int *screen_w, int *screen_h);
+void gui_mch_enable_menu(int showit);
+void gui_mch_show_toolbar(int showit);
+void gui_mch_set_dark_theme(int dark);
+int gui_mch_adjust_charheight(void);
+char_u *gui_mch_font_dialog(char_u *oldval);
+int gui_mch_init_font(char_u *font_name, int fontset);
+GuiFont gui_mch_get_font(char_u *name, int report_error);
+char_u *gui_mch_get_fontname(GuiFont font, char_u *name);
+void gui_mch_free_font(GuiFont font);
+void gui_mch_expand_font(optexpand_T *args, void *param, int (*add_match)(char_u *val));
+guicolor_T gui_mch_get_color(char_u *name);
+guicolor_T gui_mch_get_rgb_color(int r, int g, int b);
+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_mch_clear_block(int row1, int col1, int row2, int col2);
+void gui_mch_clear_all(void);
+void gui_mch_delete_lines(int row, int num_lines);
+void gui_mch_insert_lines(int row, int num_lines);
+void gui_mch_draw_hollow_cursor(guicolor_T color);
+void gui_mch_draw_part_cursor(int w, int h, guicolor_T color);
+void gui_mch_flash(int msec);
+void gui_mch_invert_rectangle(int r, int c, int nr, int nc);
+void gui_mch_update(void);
+int gui_mch_wait_for_chars(long wtime);
+void gui_mch_flush(void);
+void gui_mch_beep(void);
+void *gui_mch_get_display(void);
+void gui_mch_iconify(void);
+void gui_mch_set_foreground(void);
+void gui_mch_getmouse(int *x, int *y);
+void gui_mch_setmouse(int x, int y);
+void gui_mch_mousehide(int hide);
+int gui_mch_haskey(char_u *name);
+void gui_mch_forked(void);
+void gui_mch_enable_scrollbar(scrollbar_T *sb, int flag);
+void gui_mch_menu_grey(vimmenu_T *menu, int grey);
+void gui_mch_menu_hidden(vimmenu_T *menu, int hidden);
+void gui_mch_draw_menubar(void);
+void gui_mch_show_tabline(int showit);
+int gui_mch_showing_tabline(void);
+void gui_mch_update_tabline(void);
+void gui_mch_set_curtab(int nr);
+void gui_mch_drawsign(int row, int col, int typenr);
+void *gui_mch_register_sign(char_u *signfile);
+void gui_mch_destroy_sign(void *sign);
+int gui_gtk_draw_string_ext(int row, int col, char_u *s, int len, int flags, int force_pango);
+int gui_gtk_draw_string(int row, int col, char_u *s, int len, int flags);
+int gui_get_x11_windis(Window *win, Display **dis);
+void gui_gtk_init_socket_server(void);
+void gui_gtk_uninit_socket_server(void);
+void gui_gtk_set_mnemonics(int enable);
+void gui_make_popup(char_u *path_name, int mouse_pos);
+int get_menu_tool_width(void);
+int get_menu_tool_height(void);
+void clip_mch_request_selection(Clipboard_T *cbd);
+void clip_mch_set_selection(Clipboard_T *cbd);
+int clip_mch_own_selection(Clipboard_T *cbd);
+void clip_mch_lose_selection(Clipboard_T *cbd);
+void gui_mch_post_balloon(BalloonEval *beval, char_u *mesg);
+BalloonEval *gui_mch_create_beval_area(void *target, char_u *mesg, void (*mesgCB)(BalloonEval *, int), void *clientData);
+void gui_mch_enable_beval_area(BalloonEval *beval);
+void gui_mch_disable_beval_area(BalloonEval *beval);
+guint gtk_main_level(void);
+void gtk_main_quit(void);
+void mch_set_mouse_shape(int shape);
+void gui_mch_add_menu(vimmenu_T *menu, int idx);
+void gui_mch_add_menu_item(vimmenu_T *menu, int idx);
+void gui_mch_toggle_tearoffs(int enable);
+void gui_mch_menu_set_tip(vimmenu_T *menu);
+void gui_mch_destroy_menu(vimmenu_T *menu);
+void gui_mch_show_popupmenu(vimmenu_T *menu);
+void gui_mch_set_scrollbar_thumb(scrollbar_T *sb, long val, long size, long max);
+void gui_mch_set_scrollbar_pos(scrollbar_T *sb, int x, int y, int w, int h);
+int gui_mch_get_scrollbar_xpadding(void);
+int gui_mch_get_scrollbar_ypadding(void);
+void gui_mch_create_scrollbar(scrollbar_T *sb, int orient);
+void gui_mch_destroy_scrollbar(scrollbar_T *sb);
+void gui_mch_set_text_area_pos(int x, int y, int w, int h);
+char_u *gui_mch_browse(int saving, char_u *title, char_u *dflt, char_u *ext, char_u *initdir, char_u *filter);
+char_u *gui_mch_browsedir(char_u *title, char_u *initdir);
+int gui_mch_dialog(int type, char_u *title, char_u *message, char_u *buttons, int dfltbutton, char_u *textfield, int ex_cmd);
+void gui_mch_find_dialog(exarg_T *eap);
+void gui_mch_replace_dialog(exarg_T *eap);
+void ex_helpfind(exarg_T *eap);
+void gui_gtk4_hardcopy(exarg_T *eap);
+/* vim: set ft=c : */
index bb0b3d4f5322a88bbe62ee97d7b2d37854f7f9ab..e6bc4886cc30402d47b62f9b13c7ef599ed11c7a 100644 (file)
@@ -729,6 +729,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    501,
 /**/
     500,
 /**/
@@ -2077,7 +2079,9 @@ list_version(void)
 #if !defined(FEAT_GUI)
     msg_puts(_("without GUI."));
 #elif defined(FEAT_GUI_GTK)
-# if defined(USE_GTK3)
+# if defined(USE_GTK4)
+    msg_puts(_("with GTK4 GUI."));
+# elif defined(USE_GTK3)
     msg_puts(_("with GTK3 GUI."));
 # elif defined(FEAT_GUI_GNOME)
     msg_puts(_("with GTK2-GNOME GUI."));
index 683aff2b7585dc71c1e623e14a09e4e29d32889f..6906547c4ee891d4b8984e6a2e502ac3f62ac59c 100644 (file)
--- a/src/vim.h
+++ b/src/vim.h
@@ -2365,7 +2365,7 @@ typedef struct
     Atom       sel_atom;       // PRIMARY/CLIPBOARD selection ID
 # endif
 
-# ifdef FEAT_GUI_GTK
+# if defined(FEAT_GUI_GTK) && !defined(USE_GTK4)
     GdkAtom     gtk_sel_atom;  // PRIMARY/CLIPBOARD selection ID
 # endif