--- /dev/null
+/* 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