Add --enable-gui=gtk4 support by mattn · Pull Request #19815 · vim/vim · GitHub
Skip to content

Add --enable-gui=gtk4 support#19815

Open
mattn wants to merge 48 commits intovim:masterfrom
mattn:gtk4-gui
Open

Add --enable-gui=gtk4 support#19815
mattn wants to merge 48 commits intovim:masterfrom
mattn:gtk4-gui

Conversation

@mattn
Copy link
Copy Markdown
Member

@mattn mattn commented Mar 25, 2026

Add GTK4 GUI backend using separate source files instead of adding conditional branches to the existing GTK2/GTK3 code. GTK4's API changes are too fundamental for #if branching to be maintainable.

screenshot

This PR was developed with the assistance of Claude Code (Anthropic).

Build

./configure --enable-gui=gtk4
make

Requires GTK 4.10+ (for GtkFontDialog, GtkFileDialog, GtkAlertDialog).

New files

  • gui_gtk4.c — Main GUI implementation (window, events, drawing, fonts, colors, menus, scrollbars, dialogs, toolbar)
  • gui_gtk4_f.c/h — GtkForm widget (extends GtkWidget, uses GskTransform)

No direct X11 dependency

The GTK4 backend does not use any X11 API directly. All X11 libraries shown by ldd are indirect dependencies from libgtk-4.so.

Features

  • Window display and resizing (all directions including maximize/fullscreen)
  • Text rendering (Pango glyph string based, per-item font selection)
  • Multi-byte / CJK character support (correct fullwidth rendering)
  • Key input via GtkEventController
  • Mouse events (click, drag, release)
  • Scroll wheel
  • Background/foreground colors and colorschemes
  • Font selection dialog (:set guifont=*) via GtkFontDialog
  • File open/save dialog (:browse) via GtkFileDialog
  • Directory browser via GtkFileDialog
  • Message dialog (confirm()) via GtkAlertDialog
  • Find/Replace dialog (:promptfind / :promptrepl)
  • Scrollbar creation, show/hide, value-changed
  • Cursor blinking
  • Cursor drawing (hollow and part cursor)
  • Screen scrolling (delete/insert lines)
  • Visual bell (screen invert)
  • Clipboard (PRIMARY and CLIPBOARD via GdkClipboard)
  • Sign icons (GdkPixbuf + cairo)
  • Tabline update
  • Mouse cursor shape
  • Socket server (--remote)
  • Balloon eval (tooltip)
  • has("gui_gtk4") feature flag
  • Geometry parsing (custom parser, no X11 dependency)
  • Drag and drop (files and text via GtkDropTarget)
  • XIM input method support (GtkIMContext)
  • Toolbar (GtkBox + GtkButton + themed icons)
  • Menu system (GMenu + GtkPopoverMenuBar)
  • :q! exit
  • Plugin compatibility (vim-lsp, vim-lsp-settings)
  • Default window size 80x24

Known limitations

  • Defaults to GSK_RENDERER=cairo because GL/Vulkan renderers may not be available in all environments. Can be overridden via environment variable.
  • Suppresses EGL warnings (EGL_LOG_LEVEL=fatal) when GL is unavailable.

This is a work in progress.

@mattn mattn force-pushed the gtk4-gui branch 3 times, most recently from 2a841df to 565b319 Compare March 25, 2026 15:21
@chrisbra
Copy link
Copy Markdown
Member

@64-bitman
Copy link
Copy Markdown
Contributor

I think the GTK4 port of GVim should focus more on Wayland instead of X11, which is already deprecated and will be removed in GTK5. I suppose there are issues with keyboard layout detection, but there a much more users that have it working fine. Having a 2800 line file called "gui_gtk4_x11.c" does not feel right...

@mattn
Copy link
Copy Markdown
Member Author

mattn commented Mar 26, 2026

@chrisbra

Default GDK backend is x11

image image image image
$ GDK_BACKEND=wayland GSK_RENDERER=cairo ./vim -gf
image

@mattn
Copy link
Copy Markdown
Member Author

mattn commented Mar 26, 2026

Currently the only X11 dependency in the GTK4 backend is XParseGeometry from <X11/Xutil.h>. By implementing our own geometry parser, we can remove the X11 dependency entirely and switch the default GDK_BACKEND to Wayland.

@mattn
Copy link
Copy Markdown
Member Author

mattn commented Mar 26, 2026

This also means we could rename gui_gtk4_x11.c to gui_gtk4.c since it would no longer have any X11 dependency.

@mattn mattn force-pushed the gtk4-gui branch 2 times, most recently from 3c84d69 to cb39dfa Compare March 26, 2026 03:46
@mattn
Copy link
Copy Markdown
Member Author

mattn commented Mar 26, 2026

The GTK4 port now has zero direct X11 dependency 💪

$ nm -u ./vim | grep -i '^.\+U.*X[A-Z]' | grep -v 'gdk\|gtk\|glib\|pango\|cairo\|GLIBC\|g_\|getx'
                 U wl_proxy_add_listener
                 U wl_proxy_destroy
                 U wl_proxy_get_version
                 U wl_proxy_marshal_flags

The X11 libraries shown by ldd are all indirect dependencies pulled in by libgtk-4.so itself, not by Vim's code.

@mattn
Copy link
Copy Markdown
Member Author

mattn commented Mar 26, 2026

Multi-byte characters are now rendering correctly. Getting close to completion.

image

@mattn mattn marked this pull request as ready for review March 26, 2026 04:49
@64-bitman
Copy link
Copy Markdown
Contributor

I am getting build errors when trying to compile this PR. It seems that multiple functions in gui_gtk4.c are not exposed in the .pro file

@mattn
Copy link
Copy Markdown
Member Author

mattn commented Mar 26, 2026

Sorry, the proto file was broken when gui_gtk4_x11.c was merged into gui_gtk4.c. It should be fixed now.

@64-bitman
Copy link
Copy Markdown
Contributor

Here are some compilation errors I am getting on Linux:

getchar.c: In function ‘vgetc’:
getchar.c:2023:44: error: passing argument 1 of ‘gtk_menu_shell_select_first’ makes pointer from integer without a cast [-Wint-conversion]
 2023 |                                            GTK_MENU_SHELL(gui.menubar), FALSE);
      |                                            ^~~~~~~~~~~~~~~~~~~~~~~~~~~
      |                                            |
      |                                            GType {aka long unsigned int}
In file included from proto.h:306,
                 from vim.h:2525,
                 from getchar.c:15:
proto/gui_gtk4.pro:90:40: note: expected ‘void *’ but argument is of type ‘GType’ {aka ‘long unsigned int’}
   90 | void gtk_menu_shell_select_first(void *shell, gboolean sr);
      |                                  ~~~~~~^~~~~
optionstr.c: In function ‘expand_set_guifont’:
optionstr.c:2680:19: error: passing argument 2 of ‘expand_set_opt_callback’ from incompatible pointer type [-Wincompatible-pointer-types]
 2680 |             args, gui_mch_expand_font, &wide, numMatches, matches);
      |                   ^~~~~~~~~~~~~~~~~~~
      |                   |
      |                   int (*)(optexpand_T *, int *, char_u ***) {aka int (*)(optexpand_T *, int *, unsigned char ***)}
optionstr.c:960:16: note: expected ‘void (*)(optexpand_T *, void *, int (*)(char_u *))’ {aka ‘void (*)(optexpand_T *, void *, int (*)(unsigned char *))’} but argument is of type ‘int (*)(optexpand_T *, int *, char_u ***)’ {aka ‘int (*)(optexpand_T *, int *, unsigned char ***)’}
  960 |         void (*func)(optexpand_T *, void* params, int (*cb)(char_u *val)),
      |         ~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from proto.h:306,
                 from vim.h:2525,
                 from optionstr.c:14:
proto/gui_gtk4.pro:32:5: note: ‘gui_mch_expand_font’ declared here
   32 | int gui_mch_expand_font(optexpand_T *args, int *numMatches, char_u ***matches);
      |     ^~~~~~~~~~~~~~~~~~~

@pheiduck
Copy link
Copy Markdown
Contributor

I build w/ gtk4 overtime, but may it makes no sense until this gets merge. Thx for your work @mattn

@gonzaru
Copy link
Copy Markdown

gonzaru commented Mar 26, 2026

Thank @mattn this looks very interesting! :)

(GNU/Linux dwm x11)

This is a screenshot with the black version menu with set guioptions+=d if somebody want to see it.

vim-gtk4

I understand that this still in beta phase, For now I was able to build it without problems, but I can only start it using gvim --nofork (without --nofork, does not start).

Thanks again and good job!

@mattn
Copy link
Copy Markdown
Member Author

mattn commented Mar 27, 2026

Since I'm developing this on WSL2, I haven't been able to test the Broadway backend yet. If you have a native Linux environment, you should be able to run gvim in a browser:

  1. Start broadwayd :5
  2. Run GDK_BACKEND=broadway BROADWAY_DISPLAY=:5 ./vim -gf
  3. Open http://localhost:8085 in your browser

You should see gvim running inside the browser. I'd appreciate it if someone could give it a try!

@gonzaru
Copy link
Copy Markdown

gonzaru commented Mar 27, 2026

Hello,

Thanks for the information about broadway, I will take a look when possible.

I still can't really test it properly, but following the recent GTK4 updates, the --nofork issue is now resolved and it's not needed anymore, starts well by default without problems.

Small things that I quickly detect when I used it:

1. Empty WM_CLASS (X11)

When running in an X11 environment, the WM_CLASS attribute is empty. This prevents window managers (I use dwm) from correctly identifying the application, applying window rules etc..

Command: xprop

(gtk4)
Output: WM_CLASS(STRING) = "", ""

(gtk2, gtk3) already has this by default.
Expected: WM_CLASS(STRING) = "gvim", "Gvim" 

2. Crash when setting guioptions=k

Setting the k flag (Keep window size) in guioptions causes an immediate crash.

Steps to reproduce:

    Start gvim: gvim --clean

    Run: :set guioptions=k

Result: Segmented fault / Crash.

3. Ligatures are not rendering (works with gtk2, gktk3)

Even with a supported font and the guiligatures option set, symbols are not displayed as expected.

Steps to reproduce:

    :set guiligatures==!><

    Input text: != <= >=

Result: Characters are rendered individually rather than as a single ligature glyph.

4. X11 Feature missing in :version (maybe it's correct)

The :version output shows -X11. While this is a GTK4 build, it is unclear if certain X11-specific integrations (like clipboard) are intentionally disabled or if this is an expecting setting in the build as you said before (no X11 deps).

Thank you, this looks promising!

@mattn
Copy link
Copy Markdown
Member Author

mattn commented Mar 27, 2026

Improved the toolbar appearance by using flat style buttons, which removes the raised/bordered look.

image

@mattn
Copy link
Copy Markdown
Member Author

mattn commented Mar 27, 2026

@gonzaru Thank you for the detailed report! The three issues you reported have been fixed:

  1. WM_CLASS empty — Added g_set_prgname("gvim") so WM_CLASS is set correctly on X11.
  2. Crash on set guioptions=k — Fixed infinite recursion in gui_mch_newfont().
  3. Ligatures not rendering — Ported the ligature rendering logic from GTK3 to GTK4.

Regarding -X11 in :version: this is intentional. This GTK4 port removes direct X11 dependencies. gvim no longer depends on X11 directly. It still works on X11 servers, but only through GTK4's GDK backend abstraction, not via direct X11 API calls.

@gonzaru
Copy link
Copy Markdown

gonzaru commented Mar 27, 2026

@gonzaru Thank you for the detailed report! The three issues you reported have been fixed:

1. **WM_CLASS empty** — Added `g_set_prgname("gvim")` so WM_CLASS is set correctly on X11.

2. **Crash on `set guioptions=k`** — Fixed infinite recursion in `gui_mch_newfont()`.

3. **Ligatures not rendering** — Ported the ligature rendering logic from GTK3 to GTK4.

Regarding -X11 in :version: this is intentional. This GTK4 port removes direct X11 dependencies. gvim no longer depends on X11 directly. It still works on X11 servers, but only through GTK4's GDK backend abstraction, not via direct X11 API calls.

I can confirm that 2. and 3 are resolved. The crash is fixed and the ligatures work!

About 1. the WM_CLASS now reports:

WM_CLASS(STRING) = "gvim", "gvim"

Needs to be:

WM_CLASS(STRING) = "gvim", "Gvim"

This is how gtk2 and gkt3 also report it, as the class name (the second field) the standard is to capitalize it.

The spec states that WM_CLASS must contain two consecutive null-terminated strings:

Instance Name (gvim): Identifies the specific instance (used for specific resource lookups).

Class Name (Gvim): Identifies the general class of the application (used for grouping and general rules).

(4.1.2.5. WM_CLASS Property)
https://tronche.com/gui/x/icccm/sec-4.html

I will try to check more things this weekend and report it if necessary.

Regards

@mattn
Copy link
Copy Markdown
Member Author

mattn commented Mar 27, 2026

mattn and others added 29 commits April 24, 2026 08:27
Fix ASCII text not visible:
- Add missing cairo_set_source_rgba and pango_cairo_show_glyph_string
  calls in the ASCII fast path of gui_gtk_draw_string.

Fix CJK/wide character rendering:
- Process each PangoItem separately in gui_gtk_draw_string to use
  the correct font for each script segment (fixes tofu for Japanese).
- Calculate glyph width based on utf_ptr2cells() so fullwidth
  characters get char_width * 2.

Fix hollow cursor on startup:
- Call gui_focus_change(TRUE) in gui_mch_open to ensure Vim knows
  the window has focus, so a solid block cursor is drawn instead
  of a hollow one.

Co-authored-by: Claude <noreply@anthropic.com>
Regenerate proto/gui_gtk4.pro after gui_gtk4_x11.c was merged into
gui_gtk4.c.  Also add gui_gtk4.pro to ALL_GUI_PRO and the dependency
rules in the Makefile so `make proto` picks it up.
The GTK_MENU_SHELL() stub was defined with no parameters, but callers
in getchar.c pass a widget pointer (matching the original GTK macro
signature).  This caused a compilation error.
gui_get_x11_windis() was declared as void but gui.c expects an int
return value (compared against OK).  Return FAIL since X11 window/
display are not applicable in GTK4.
GTK4 does not have GtkMenuShell.  Instead of providing stub functions
for GTK_MENU_SHELL() and gtk_menu_shell_select_first(), guard the
F10 menu activation code in getchar.c with !defined(USE_GTK4) and
remove the stubs from gui_gtk4.c.
gtk_widget_show() is deprecated in GTK4.  Use gtk_widget_set_visible()
with TRUE instead.
- Replace gtk_widget_show/hide() with gtk_widget_set_visible()
- Replace gtk_style_context_add_provider() with
  gtk_style_context_add_provider_for_display()
- Guard gtk_widget_get_style_context() with #ifndef USE_GTK4
GTK4 does not ship gdkkeysyms-compat.h, so legacy symbols like
GDK_Shift_L and GDK_MOD1_MASK are not available.  Use GDK_KEY_Shift_L
and GDK_ALT_MASK under USE_GTK4.
GTK4's GtkIMContext does not support synthesizing key events for IM
activation, so 'imactivatekey' cannot work.  Return FALSE for non-empty
values so that invalid settings are rejected instead of silently ignored.
Calling gtk_init() before fork() breaks the display connection in the
child process, making gvim fail to start without --nofork.  Move the
call to gui_mch_init_check() which is called after fork(), matching
the GTK3 pattern of deferring gtk_init_check() until after fork.
gui_mch_newfont() was calling gui_set_shellsize() which called
gui_mch_newfont() back, causing infinite recursion and a crash.
Use gui_resize_shell() instead to recalculate Rows/Columns from the
current window size without re-entering gui_set_shellsize().

Also add a FIXME comment to gui.c where gui_mch_newfont() is called
even when the font hasn't changed (e.g. just setting guioptions=k).
Without this, WM_CLASS is empty on X11, which prevents window managers
like dwm from identifying gvim windows for placement rules.
Port the ligature rendering logic from gui_gtk_x11.c (GTK3) to GTK4.
Previously ligature characters were rendered individually through the
fast ASCII glyph cache path, ignoring the ligatures_map entirely.

Split gui_gtk_draw_string into a wrapper that segments the string into
ASCII and ligature/UTF-8 parts, and gui_gtk_draw_string_ext that does
the actual drawing with proper Pango shaping via pango_shape_full().
Also port helper functions for cluster-based glyph width calculation,
combining character handling, and guifontwide support.
Two issues prevented mouse events from reaching Vim:

1. gui.formwin (GtkForm overlay for scrollbars) was layered on top of
   gui.drawarea via GtkOverlay, intercepting all mouse events.
   Fix: set can_target=FALSE on formwin so events pass through.

2. Drag detection used `if (mouse_pressed_button)` but MOUSE_LEFT is
   0x00, so left-button drags were never detected.
   Fix: use -1 as "no button pressed" sentinel and check >= 0.
gui_mch_flush() only called gdk_display_flush() which flushes the
display buffer but does not notify GTK that the offscreen surface
content has changed. Add gtk_widget_queue_draw() so the draw_event
callback is triggered to paint the updated surface to the widget.
When focus moved to another widget (e.g. menubar, toolbar), the
drawing area did not regain focus on mouse re-enter, leaving the
cursor in its unfocused (hollow) shape. Add gtk_widget_grab_focus()
in enter_notify_event, matching the GTK3 behavior.
When the drawing area was resized (e.g. after :vsp), the old surface
content was copied to the new surface. If Vim's redraw didn't cover
all areas, stale content like the intro screen text remained as ghost
artifacts. Fix by filling the new surface with the background color
instead of copying old content, since gui_resize_shell() triggers a
full redraw anyway. Also remove duplicate surface resizing logic from
draw_event, centralizing it in drawarea_resize_cb.
WM_CLASS is an X11 property that GTK4 no longer sets directly.
The correct way to associate windows with the application in GTK4
is through the StartupWMClass field in the desktop entry file.
When 'guioptions' includes "c" and 'confirm' is set, :q on a modified
buffer sets exiting=TRUE before calling do_dialog().  The GTK4-specific
check for "exiting" in gui_mch_wait_for_chars() caused it to bail out
immediately without waiting for key input, resulting in an infinite
loop.  Remove the "exiting" check to match the GTK3 implementation.
The button label strings parsed from the "&Yes\n&No\n&Cancel" format
were stored as pointers into a buffer that was freed before
gtk_alert_dialog_set_buttons() copied them.  Defer vim_free(buf) and
CONVERT_TO_UTF8_FREE() until after the dialog is done.
Implement gui_mch_menu_grey() for GTK4 using g_simple_action_set_enabled()
to grey out disabled menu items (e.g. Copy/Paste when unavailable).

Store the GAction name in menu->label for later lookup.

Also fix the popover menu not closing after selecting an item.
GTK4's GtkPopoverMenuBar marks popovers as not-visible but Vim's
custom main loop does not process the rendering update.
Use gtk_widget_unrealize() to force the popover surface to close.
GTK4 does not provide a window position API (removed by design for
Wayland compatibility). Return FAIL instead of reporting 0,0 which
is misleading.
List monospace font families from Pango context for :set guifont=<TAB>
completion, matching the GTK3 implementation.
Use GtkPrintOperation with Pango/Cairo rendering instead of
PostScript + lpr. Guarded by FEAT_GUI_GTK_PRINT ifdef.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants