/* 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"
#include "gui_gtk4_cb.h"
#ifdef USE_GTK4_SNAPSHOT
# include "gui_gtk4_da.h"
#endif
#ifdef FEAT_TOOLBAR
# include "gui_gtk4_tb.h"
#endif
#ifdef FEAT_MENU
# include "gui_gtk4_menu.h"
#endif

/*
 * 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;
}

#if defined(FEAT_MOUSESHAPE)
// Last set mouse pointer shape
static int last_shape = 0;
#endif

#define DEFAULT_FONT	"Monospace 10"

// 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
#ifndef USE_GTK4_SNAPSHOT
static void draw_event(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data);
#endif
static gboolean key_press_event(GtkEventControllerKey *controller, guint keyval, guint keycode, GdkModifierType state, gpointer data);
static void key_release_event(GtkEventControllerKey *controller, guint keyval, guint keycode, GdkModifierType state, gpointer data);
static void button_press_event(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data);
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 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(GtkDropTargetAsync *target, GdkDrop *drop, double x, double y, void *data);
#endif
#ifdef FEAT_GUI_TABLINE
static void tabline_enter_cb(GtkEventController *controller, double x, double y, void *udata);
static void on_select_tab(GtkNotebook *notebook, gpointer *page, gint idx, gpointer data);
static void on_tab_reordered(GtkNotebook *notebook, gpointer *page, gint idx, gpointer data);
static VimMenu *create_tabline_popup_menu(void);
static void tabline_menu_press_event(GtkGestureClick *gesture, int n_press, double x, double y, GtkWidget *popover);
#endif
static void mainwin_destroy_cb(GObject *object, gpointer data);
static gboolean delete_event_cb(GtkWindow *window, gpointer data);
static int query_pointer_pos(int *x, int *y, GdkModifierType *state);
static void mainwin_fullscreened_cb(GObject *obj, GParamSpec *pspec, gpointer user_data);
static void drawarea_realize_cb(GtkWidget *widget, gpointer data);
static void drawarea_unrealize_cb(GtkWidget *widget, gpointer data);
#ifndef USE_GTK4_SNAPSHOT
static void drawarea_scale_factor_cb(GObject *object, GParamSpec *pspec, gpointer data);
static cairo_surface_t *create_backing_surface(int width, int height);
#endif
static void clipboard_changed_cb(GdkClipboard *clipboard, gpointer user_data);
#ifdef FEAT_MENU
static void show_menubar_popover(void);
#endif

/*
 * 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_new0(GdkRGBA, 1);
    gui.bgcolor = g_new0(GdkRGBA, 1);
    gui.spcolor = g_new0(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);
    g_signal_connect(G_OBJECT(gui.mainwin), "notify::fullscreened",
		     G_CALLBACK(mainwin_fullscreened_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
    gui.menubar = vim_menu_bar_new();
    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 = vim_toolbar_new();
    gtk_widget_set_name(gui.toolbar, "vim-toolbar");
    gtk_widget_set_visible(gui.toolbar, FALSE);
    gtk_box_append(GTK_BOX(vbox), gui.toolbar);
    vim_toolbar_set_style(VIM_TOOLBAR(gui.toolbar), toolbar_flags, tbis_flags);
#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), TRUE);
    gtk_notebook_set_scrollable(GTK_NOTEBOOK(gui.tabline), TRUE);
    gtk_widget_set_visible(gui.tabline, FALSE);
    gtk_box_append(GTK_BOX(vbox), gui.tabline);

    g_signal_connect(G_OBJECT(gui.tabline), "switch-page",
		     G_CALLBACK(on_select_tab), NULL);
    g_signal_connect(G_OBJECT(gui.tabline), "page-reordered",
		     G_CALLBACK(on_tab_reordered), NULL);

    {
	GtkEventController *mcontroller = gtk_event_controller_motion_new();

	g_signal_connect(mcontroller, "enter",
		G_CALLBACK(tabline_enter_cb), NULL);

	// Make sure that tabline_enter_cb() is always called before
	// tabpage_enter_cb().
	gtk_event_controller_set_propagation_phase(
		mcontroller, GTK_PHASE_CAPTURE);
	gtk_widget_add_controller(gui.tabline, mcontroller);
    }

    // Create right click popup menu for tabline
    {
	GtkGesture	*click;
	VimMenu		*menu;

	click = gtk_gesture_click_new();
	menu = create_tabline_popup_menu();

	gtk_widget_set_parent(GTK_WIDGET(menu), gui.tabline);
	g_object_set_data(G_OBJECT(gui.tabline), "menu", menu);

	gtk_popover_set_has_arrow(GTK_POPOVER(menu), FALSE);
	gtk_popover_set_position(GTK_POPOVER(menu), GTK_POS_BOTTOM);
	// Make popover start at top left corner
	gtk_widget_set_halign(GTK_WIDGET(menu), GTK_ALIGN_START);

	// Listen for anny mouse button
	gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click), 0);

	g_signal_connect_object(click, "pressed",
		G_CALLBACK(tabline_menu_press_event),
		menu, G_CONNECT_DEFAULT);
	gtk_widget_add_controller(gui.tabline, GTK_EVENT_CONTROLLER(click));
    }
#endif

    // The form widget manages absolute positioning of scrollbars and the draw
    // area.
    gui.formwin = vim_form_new();
    gtk_widget_set_name(gui.formwin, "vim-gtk-form");
    gtk_widget_set_vexpand(gui.formwin, TRUE);
    gtk_widget_set_hexpand(gui.formwin, TRUE);
    gtk_box_append(GTK_BOX(vbox), gui.formwin);

    // The drawing area for the editor content.
#ifdef USE_GTK4_SNAPSHOT
    gui.drawarea = vim_draw_area_new();
#else
    gui.drawarea = gtk_drawing_area_new();
    gui.surface = NULL;
#endif
    gtk_widget_set_focusable(gui.drawarea, TRUE);
    gtk_widget_set_vexpand(gui.drawarea, TRUE);
    gtk_widget_set_hexpand(gui.drawarea, TRUE);
    vim_form_put(VIM_FORM(gui.formwin), gui.drawarea, 0, 0);

#ifndef USE_GTK4_SNAPSHOT
    // Set up drawing.
    gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(gui.drawarea),
	    (GtkDrawingAreaDrawFunc)draw_event, NULL, NULL);

    g_signal_connect(G_OBJECT(gui.drawarea), "notify::scale-factor",
		     G_CALLBACK(drawarea_scale_factor_cb), NULL);
#endif
    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);

    // 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.drawarea, key_ctrl);
#ifdef FEAT_XIM
	xim_init();
#endif
    }

    {
	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);
	gtk_widget_add_controller(gui.drawarea, motion);
    }

    {
	GtkEventController *scroll = gtk_event_controller_scroll_new(
		GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES
		| GTK_EVENT_CONTROLLER_SCROLL_DISCRETE);
	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. We use async variant so
    // we can handle html format.
    {
	gui.drop_target = gtk_drop_target_async_new(NULL,
		GDK_ACTION_COPY | GDK_ACTION_MOVE);

	gui_gtk_set_dnd_targets();
	g_signal_connect(gui.drop_target, "drop",
			 G_CALLBACK(drop_cb), NULL);
	gtk_widget_add_controller(gui.drawarea,
		GTK_EVENT_CONTROLLER(gui.drop_target));
    }
#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);

    {
	GdkDisplay   *display = gtk_widget_get_display(gui.mainwin);
	GdkClipboard *primary = gdk_display_get_primary_clipboard(display);
	GdkClipboard *regular = gdk_display_get_clipboard(display);

	if (primary != NULL)
	    g_signal_connect(primary, "changed",
		    G_CALLBACK(clipboard_changed_cb), &clip_star);
	if (regular != NULL)
	    g_signal_connect(regular, "changed",
		    G_CALLBACK(clipboard_changed_cb), &clip_plus);
    }

    gui.regular_provider = vim_content_provider_new(&clip_plus);
    gui.primary_provider = vim_content_provider_new(&clip_star);

    return OK;
}

#ifndef USE_GTK4_SNAPSHOT
/*
 * 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);
    }
}
#endif

    void
gui_mch_new_colors(void)
{
#ifndef USE_GTK4_SNAPSHOT
    surface_fill_bg();
#endif
    if (gui.drawarea != NULL && gtk_widget_get_realized(gui.drawarea))
	gtk_widget_queue_draw(gui.drawarea);
}

/*
 * 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)
    {
#ifdef FEAT_GUI_TABLINE
	// Must unparent popover menu for tabline or we will get warning
	gtk_widget_unparent(g_object_get_data(G_OBJECT(gui.tabline), "menu"));
#endif
#ifdef FEAT_BEVAL_GUI
	// Make sure to destroy popover used for balloon eval, or we will get a
	// warning from GTK that the draw area still has children left.
	gui_mch_destroy_beval_area(balloonEval);
#endif
#ifdef FEAT_MENU
	// Make sure to unparent any popover menus
	{
	    vimmenu_T *menu;

	    FOR_ALL_MENUS(menu)
	    {
		if ((menu->name[0] == ']' || menu_is_popup(menu->name))
			&& menu->submenu_id != NULL)
		    gtk_widget_unparent(menu->submenu_id);
	    }
	}
#endif
	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));
}

    void
gui_mch_set_fullscreen(int flag)
{
    if (gui.mainwin == NULL)
	return;
    if (flag)
	gtk_window_fullscreen(GTK_WINDOW(gui.mainwin));
    else
	gtk_window_unfullscreen(GTK_WINDOW(gui.mainwin));
}

    static void
mainwin_fullscreened_cb(GObject *obj,
	GParamSpec *pspec UNUSED, gpointer user_data UNUSED)
{
    // Force a redraw of the drawing area when entering fullscreen mode.
    if (gtk_window_is_fullscreen(GTK_WINDOW(obj)))
	gui_focus_change(TRUE);
}

/*
 * 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;

    // Do not subtract width and height with menubar, toolbar, etc, because
    // those are not part of the shell.
    w = gtk_widget_get_width(gui.formwin);
    h = gtk_widget_get_height(gui.formwin);
    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);
}

/*
 * Get height of window decorations, that we cannot determine directly. For
 * example, the GtkHeaderBar widget. This is called in gui_resize_shell(), we
 * cannot call it in gui_set_shellsize(), because that may be called before the
 * drawarea/formwin is resized, which may cause the drawarea to be bigger than
 * it actually is (while the window size is up to date), causing a negative
 * "decor_height".
 */
    void
gui_gtk_init_decor_height(void)
{
    int h = gtk_widget_get_height(gui.mainwin);

    if (h == 0)
	return;

    h -= get_menu_tool_height();
    h -= gtk_widget_get_height(gui.formwin);

    gui.decor_height = h;
}

    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)
{
    width += get_menu_tool_width();
    height += get_menu_tool_height();

    // GtkWindow default size also includes client side decorations, so must
    // include it also.
    height += gui.decor_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);
	// Draw area might become blank after this for some reason, queue a
	// redraw, same for toolbar as well.
	if (gui.drawarea != NULL)
	    gtk_widget_queue_draw(gui.drawarea);
    }
}
#endif

#ifdef FEAT_TOOLBAR
    void
gui_mch_show_toolbar(int showit)
{
    if (gui.toolbar != NULL)
    {
	gtk_widget_set_visible(gui.toolbar, showit);
	if (showit)
	    vim_toolbar_set_style(VIM_TOOLBAR(gui.toolbar),
		    toolbar_flags, tbis_flags);
	if (gui.drawarea != NULL)
	    gtk_widget_queue_draw(gui.drawarea);
    }
}
#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();

    // im window position depends on cursor size which depends on font metrics
    // update the position after we've initialized font
    im_set_position(gui.row, gui.col);

    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
 * ============================================================
 */

#ifdef USE_GTK4_SNAPSHOT
    void
gui_gtk4_update_size(void)
{
    vim_draw_area_set_size(VIM_DRAW_AREA(gui.drawarea),
	    gui.num_rows, gui.num_cols);
}

# ifdef FEAT_NETBEANS_INTG
    cairo_t *
gui_gtk4_get_multisign_context(int x, int y, int w, int h)
{
    return vim_draw_area_get_multisign_cairo(
	    VIM_DRAW_AREA(gui.drawarea), x, y, w, h);
}
# endif
#else // USE_GTK4_SNAPSHOT
static void set_cairo_source_from_pixel(cairo_t *cr, guicolor_T pixel);

    static void
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);
}

    static int
get_drawarea_scale(void)
{
    int scale = 1;

    if (gui.drawarea != NULL)
	scale = gtk_widget_get_scale_factor(gui.drawarea);
    if (scale < 1)
	scale = 1;
    return scale;
}

    static cairo_surface_t *
create_backing_surface(int width, int height)
{
    cairo_surface_t *surf;
    int		    scale;

    if (width <= 0 || height <= 0)
	return NULL;

    scale = get_drawarea_scale();
    surf = cairo_image_surface_create(
	    CAIRO_FORMAT_ARGB32, width * scale, height * scale);
    cairo_surface_set_device_scale(surf, (double)scale, (double)scale);
    return surf;
}
#endif // !USE_GTK4_SNAPSHOT

    void
gui_mch_clear_block(int row1, int col1, int row2, int col2)
{
#ifdef USE_GTK4_SNAPSHOT
    vim_draw_area_clear_block(VIM_DRAW_AREA(gui.drawarea), row1,
	    col1, row2, col2);
#else
    cairo_t *cr;

    if (gui.surface == NULL)
	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);
#endif

    if (gui.drawarea != NULL)
	gtk_widget_queue_draw(gui.drawarea);
}

    void
gui_mch_clear_all(void)
{
#ifdef USE_GTK4_SNAPSHOT
    vim_draw_area_clear(VIM_DRAW_AREA(gui.drawarea));
#else
    cairo_t *cr;

    if (gui.surface == NULL)
	return;

    cr = cairo_create(gui.surface);
    set_cairo_source_from_pixel(cr, gui.back_pixel);
    cairo_paint(cr);
    cairo_destroy(cr);
#endif

    if (gui.drawarea != NULL)
	gtk_widget_queue_draw(gui.drawarea);
}

#ifdef FEAT_IMAGE_GDK
    void
gui_gtk4_remove_image(win_T *wp)
{
    vim_draw_area_remove_image(VIM_DRAW_AREA(gui.drawarea), wp->w_id);
}

    void
gui_mch_free_popup_image(win_T *wp)
{
    if (wp->w_popup_image_texture != NULL)
	g_clear_object(&wp->w_popup_image_texture);
}

/*
 * If "wp->w_popup_image_texture" is NULL or "force" is TRUE, then create the
 * cached GdkTexture object.
 */
    static void
maybe_set_image_texture(win_T *wp, gboolean force)
{
    GdkMemoryFormat fmt;
    size_t	    stride;
    GdkTexture	    *texture;
    GBytes	    *bytes;
    size_t	    size;

    if (!force && wp->w_popup_image_texture != NULL)
	return;

    if (wp->w_popup_image_alpha)
    {
	fmt = GDK_MEMORY_A8R8G8B8;
	size = wp->w_popup_image_w * wp->w_popup_image_h * 4;
	stride = wp->w_popup_image_w * 4;
    }
    else
    {
	fmt = GDK_MEMORY_R8G8B8;
	size = wp->w_popup_image_w * wp->w_popup_image_h * 3;
	stride = wp->w_popup_image_w * 3;
    }

    bytes = g_bytes_new(wp->w_popup_image_data, size);
    texture = gdk_memory_texture_new(wp->w_popup_image_w,
	    wp->w_popup_image_h, fmt, bytes, stride);
    g_bytes_unref(bytes);

    if (wp->w_popup_image_texture != NULL)
	g_object_unref(wp->w_popup_image_texture);
    wp->w_popup_image_texture = texture;
}

    bool
gui_mch_update_popup_image_pixels(win_T *wp)
{
    if (wp->w_popup_image_texture == NULL || wp->w_popup_image_data == NULL)
	return false;
    maybe_set_image_texture(wp, TRUE);
    return true;
}

    void
gui_mch_draw_popup_image(
	win_T	*wp,
	int	 row,
	int	 col,
	int	 src_x,
	int	 src_y,
	int	 draw_w,
	int	 draw_h)
{
    if (wp->w_popup_image_data == NULL
	    || wp->w_popup_image_w <= 0 || wp->w_popup_image_h <= 0
	    || draw_w <= 0 || draw_h <= 0)
	return;

    maybe_set_image_texture(wp, FALSE);
    if (gui.drawarea != NULL)
    {
	vim_draw_area_add_image(VIM_DRAW_AREA(gui.drawarea),
		wp->w_popup_image_texture, row, col, src_x, src_y,
		draw_w, draw_h, wp->w_zindex, wp->w_id);

	gtk_widget_queue_draw(gui.drawarea);
    }
}
#endif

#ifdef FEAT_IMAGE_CAIRO
    void
gui_mch_free_popup_image(win_T *wp)
{
    cairo_popup_image_free(wp);
}

    bool
gui_mch_update_popup_image_pixels(win_T *wp)
{
    return cairo_popup_image_update(wp);
}

    void
gui_mch_draw_popup_image(
	win_T	*wp,
	int	 row,
	int	 col,
	int	 src_x,
	int	 src_y,
	int	 draw_w,
	int	 draw_h)
{
    int x, y;

    if (wp->w_popup_image_data == NULL
	    || wp->w_popup_image_w <= 0 || wp->w_popup_image_h <= 0
	    || draw_w <= 0 || draw_h <= 0
	    || gui.surface == NULL
	    )
	return;

    x = FILL_X(col);
    y = FILL_Y(row);
    cairo_popup_image_paint(wp, gui.surface, x, y,
					    src_x, src_y, draw_w, draw_h);

    if (gui.drawarea != NULL)
	gtk_widget_queue_draw(gui.drawarea);
}
#endif // FEAT_IMAGE_CAIRO

#ifndef USE_GTK4_SNAPSHOT
    static void
surface_copy_rect(int dest_x, int dest_y,
	int src_x, int src_y,
	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 = create_backing_surface(width, height);
    if (tmp == NULL)
	return;
    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);
}
#endif

    void
gui_mch_delete_lines(int row, int num_lines)
{
#ifdef USE_GTK4_SNAPSHOT
    vim_draw_area_move_block(VIM_DRAW_AREA(gui.drawarea),
	    row, row + num_lines, gui.scroll_region_bot,
	    gui.scroll_region_left, gui.scroll_region_right);
#else
    int ncols = gui.scroll_region_right - gui.scroll_region_left + 1;
    int nrows = gui.scroll_region_bot - row + 1;
    int src_nrows = nrows - num_lines;

    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);
#endif
    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)
{
#ifdef USE_GTK4_SNAPSHOT
    vim_draw_area_move_block(VIM_DRAW_AREA(gui.drawarea),
	    row + num_lines, row, gui.scroll_region_bot - num_lines,
	    gui.scroll_region_left, gui.scroll_region_right);
#else
    int ncols = gui.scroll_region_right - gui.scroll_region_left + 1;
    int nrows = gui.scroll_region_bot - row + 1;
    int src_nrows = nrows - num_lines;

    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);
#endif
    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)
{
#ifdef USE_GTK4_SNAPSHOT
    gui_mch_set_fg_color(color);
    vim_draw_area_set_hollow_cursor(VIM_DRAW_AREA(gui.drawarea));
#else
    cairo_t *cr;
    int i = 1;

    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);
#endif

    gtk_widget_queue_draw(gui.drawarea);
}

    void
gui_mch_draw_part_cursor(int w, int h, guicolor_T color)
{
#ifdef USE_GTK4_SNAPSHOT
    gui_mch_set_fg_color(color);
    vim_draw_area_set_part_cursor(VIM_DRAW_AREA(gui.drawarea), w, h);
#else
    cairo_t *cr;

    if (gui.surface == NULL)
	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);
#endif

    gtk_widget_queue_draw(gui.drawarea);
}

    void
gui_mch_flash(int msec)
{
    // Invert the screen, wait, then invert back
#ifndef USE_GTK4_SNAPSHOT
    if (gui.surface == NULL)
	return;
#endif

    gui_mch_invert_rectangle(0, 0, (int)Rows - 1, (int)Columns - 1);
    gui_mch_flush();
    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)
{
#ifdef USE_GTK4_SNAPSHOT
    vim_draw_area_invert_block(VIM_DRAW_AREA(gui.drawarea), r, c, nr, nc);
#else
    cairo_t *cr;

    if (gui.surface == NULL)
	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);
#endif

    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

#ifdef FEAT_MENU
    if (key_sym == GDK_KEY_F10 && gui.menubar != NULL)
    {
	static char_u k10[] = {K_SPECIAL, 'k', ';', 0};

	if (check_map(k10, State, FALSE, TRUE, FALSE, NULL, NULL) == NULL)
	{
	    show_menubar_popover();
	    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;
    }

#ifdef FEAT_MENU
    // If there is a menu and 'wak' is "yes", or 'wak' is "menu" and the key
    // is a menu shortcut, we ignore everything with the ALT modifier.
    if ((state & GDK_ALT_MASK)
	    && gui.menu_is_active
	    && (*p_wak == 'y'
		|| (*p_wak == 'm'
		    && len == 1
		    && gui_is_menu_shortcut(string[0]))))
	// Tell GTK we have not handled the key (so it can handle it).
	return FALSE;
#endif

    // 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 guint motion_repeat_timer = 0;
static gboolean motion_repeat_offset = FALSE;
static int mouse_press_x = -1, mouse_press_y = -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_press_x = (int)x;
    mouse_press_y = (int)y;

    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);

    // If we are repeating motion events, then stop
    if (motion_repeat_timer != 0)
    {
	timeout_remove(motion_repeat_timer);
	motion_repeat_timer = 0;
    }

    mouse_pressed_button = -1;
    gui_send_mouse_event(MOUSE_RELEASE, (int)x, (int)y, FALSE, vim_modifiers);
}

static double prev_mouse_x = -1.0;
static double prev_mouse_y = -1.0;

static GdkModifierType cur_state = 0;

    static timeout_cb_type
mouse_repeat_timer_cb(gpointer data)
{
    int x, y;

    if (mouse_pressed_button == -1)
    {
	motion_repeat_timer = 0;
	return G_SOURCE_REMOVE;
    }

    // If there already is a mouse click in the input buffer, wait another
    // time (otherwise we would create a backlog of clicks)
    if (vim_used_in_input_buf() > 10)
	return G_SOURCE_CONTINUE;

    x = (int)prev_mouse_x;
    y = (int)prev_mouse_y;

    // Fake a motion event.
    //
    // Trick: Pretend the mouse moved to the next character on every other
    // event, otherwise drag events will be discarded, because they are still in
    // the same character.
    if (motion_repeat_offset)
	x += gui.char_width;
    motion_repeat_offset = !motion_repeat_offset;

    gui_send_mouse_event(MOUSE_DRAG, x, y, FALSE,
	    modifiers_gdk2mouse(cur_state));

    // Always continue, when button is released, then this timer is removed.
    return G_SOURCE_CONTINUE;
}

    static void
motion_notify_event(GtkEventControllerMotion *controller UNUSED,
	double x, double y, gpointer data UNUSED)
{
    if (mouse_pressed_button >= 0)
    {
	GdkModifierType state;
	GdkEvent	*event;
	int		w, h;

	event = gtk_event_controller_get_current_event(
		GTK_EVENT_CONTROLLER(controller));

	if (event != NULL)
	{
	    cur_state = state = gdk_event_get_modifier_state(event);
	    gui_send_mouse_event(MOUSE_DRAG, (int)x, (int)y,
		    FALSE, modifiers_gdk2mouse(state));
	}

	// Only start repeating motion if pointer is outside of draw area (and
	// mouse button is being pressed down). Make frequency of motion
	// repeats depend on how far away the pointer is from the start of the
	// drag.
	//
	w = gtk_widget_get_width(gui.drawarea);
	h = gtk_widget_get_height(gui.drawarea);

	if (motion_repeat_timer > 0)
	    timeout_remove(motion_repeat_timer);

	if (x < 0 || y < 0 || x >= w || y >= h)
	{
	    int	dx, dy;
	    int offshoot;
	    int	delay;

	    dx = x < 0 ? -x + mouse_press_x : x - mouse_press_x;
	    dy = y < 0 ? -y + mouse_press_y : y - mouse_press_y;

	    offshoot = dx > dy ? dx : dy;

	    if (offshoot > 127)
		delay = 5;
	    else
		delay = (130 * (127 - offshoot)) / 127 + 5;

	    motion_repeat_timer = timeout_add(delay,
		    mouse_repeat_timer_cb, NULL);
	}
	else
	    motion_repeat_timer = 0;
    }
    else
	gui_mouse_moved((int)x, (int)y);

    // Only unhide if mouse actually moved. GTK seems to send a motion event
    // when switching tabs, causing the cursor to unhide.
    if (p_mh && ((prev_mouse_x == -1 || prev_mouse_y == -1)
		|| (fabs(prev_mouse_x - x) > 0.05
		    && fabs(prev_mouse_y - y) > 0.05)))
	gui_mch_mousehide(FALSE);

    prev_mouse_x = x;
    prev_mouse_y = y;

    // Make sure keyboard input goes to the drawing area. Fixes issues with menu
    // still being focused.
    gtk_widget_grab_focus(gui.drawarea);
}

    static void
enter_notify_event(GtkEventControllerMotion *controller UNUSED,
	double x, double y, gpointer data UNUSED)
{
    prev_mouse_x = x;
    prev_mouse_y = y;

    // Make sure keyboard input goes to the drawing area.
    gtk_widget_grab_focus(gui.drawarea);
}

    static gboolean
scroll_event(GtkEventControllerScroll *controller UNUSED,
	double dx UNUSED, double dy, gpointer data UNUSED)
{
    int button;
    int_u vim_modifiers;
    int x, y;
    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);

    gui_mch_getmouse(&x, &y);
    gui_send_mouse_event(button, x, y, FALSE, vim_modifiers);

    return TRUE;
}

    static void
focus_in_event(GtkEventControllerFocus *controller UNUSED,
	gpointer data UNUSED)
{
    if (gtk_window_is_active(GTK_WINDOW(gui.mainwin)))
    {
	gui_focus_change(TRUE);
	if (blink_state == BLINK_NONE)
	    gui_mch_start_blink();
    }
}

    static void
focus_out_event(GtkEventControllerFocus *controller UNUSED,
	gpointer data UNUSED)
{
    if (!gtk_window_is_active(GTK_WINDOW(gui.mainwin)))
    {
	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)
{
#ifndef USE_GTK4_SNAPSHOT
    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 = create_backing_surface(w, h);
#endif

    gui_mch_new_colors();
}

    static void
drawarea_unrealize_cb(GtkWidget *widget UNUSED, gpointer data UNUSED)
{
#ifdef FEAT_XIM
    im_shutdown();
#endif
#ifndef USE_GTK4_SNAPSHOT
    if (gui.surface != NULL)
    {
	cairo_surface_destroy(gui.surface);
	gui.surface = NULL;
    }
#endif
}

#ifndef USE_GTK4_SNAPSHOT
    void
gui_gtk4_resize(int width, int height)
{
    cairo_t *cr;
    cairo_surface_t *old_surface;
    int scale = get_drawarea_scale();

    if (width <= 0 || height <= 0)
	return;

    // Keep the backing surface in sync with the drawing area so GTK keeps
    // showing the previous frame. Re-creating it preserves the old
    // contents.
    if (gui.surface != NULL)
    {
	int sw = cairo_image_surface_get_width(gui.surface) / scale;
	int sh = cairo_image_surface_get_height(gui.surface) / scale;
	if (sw != width || sh != height)
	{
	    old_surface = gui.surface;
	    gui.surface = create_backing_surface(width, height);
	    if (gui.surface != NULL)
	    {
		cr = cairo_create(gui.surface);
		set_cairo_source_from_pixel(cr, gui.back_pixel);
		cairo_paint(cr);
		cairo_set_source_surface(cr, old_surface, 0, 0);
		cairo_paint(cr);
		cairo_destroy(cr);
	    }
	    cairo_surface_destroy(old_surface);
	}
    }
    else
    {
	gui.surface = create_backing_surface(width, height);
	if (gui.surface != NULL)
	{
	    cr = cairo_create(gui.surface);
	    set_cairo_source_from_pixel(cr, gui.back_pixel);
	    cairo_paint(cr);
	    cairo_destroy(cr);
	}
    }
}

    static void
drawarea_scale_factor_cb(GObject *object UNUSED,
	GParamSpec *pspec UNUSED, gpointer data UNUSED)
{
    int	w, h;

    if (gui.drawarea == NULL)
	return;

    w = gtk_widget_get_width(gui.drawarea);
    h = gtk_widget_get_height(gui.drawarea);
    if (w <= 0 || h <= 0)
	return;

    if (gui.surface != NULL)
	cairo_surface_destroy(gui.surface);
    gui.surface = create_backing_surface(w, h);

    if (gui.surface != NULL)
    {
	cairo_t *cr = cairo_create(gui.surface);
	set_cairo_source_from_pixel(cr, gui.back_pixel);
	cairo_paint(cr);
	cairo_destroy(cr);
    }
    gtk_widget_queue_draw(gui.drawarea);
    if (gui.in_use)
	redraw_all_later(UPD_CLEAR);
}
#endif

typedef enum
{
    READ_DATA_CLIP,	    // Clipboard data
    READ_DATA_DROP,	    // URI or text DND data
    READ_DATA_DROP_DATA	    // HTML DND data
} ReadDataType;

#define READDATA_BUFSIZE 4096

// Used for reading clipboard and DND data.
typedef struct
{
    ReadDataType    type;

    GCancellable    *cancel;
    GByteArray	    *arr; // NULL when not needed
    char	    *buf; // NULL when not needed
} ReadData;

#ifdef FEAT_DND
typedef struct
{
    ReadData	rd;
    GdkDrop	*drop;
    int		x;
    int		y;
} DropReadData;
#endif

typedef struct
{
    ReadData	rd;
    Clipboard_T *cbd;
    char	*mime_type;
} ClipReadData;

    static ReadData *
read_data_new(ReadDataType type, size_t sz)
{
    ReadData *rd = g_malloc0(sz);

    rd->type = type;
    rd->cancel = g_cancellable_new();

    if (type == READ_DATA_CLIP || type == READ_DATA_DROP_DATA)
    {
	rd->arr = g_byte_array_new();
	rd->buf = g_malloc(READDATA_BUFSIZE);
    }

    return rd;
}

    static void
read_data_free(ReadData *rd)
{
    g_cancellable_cancel(rd->cancel);

    if (rd->type == READ_DATA_CLIP)
	g_free(((ClipReadData *)rd)->mime_type);
    else if (rd->type == READ_DATA_DROP || rd->type == READ_DATA_DROP_DATA)
	g_object_unref(((DropReadData *)rd)->drop);
    g_object_unref(rd->cancel);
    if (rd->arr != NULL)
	g_byte_array_free(rd->arr, TRUE);
    g_free(rd->buf);
    g_free(rd);
}

#ifdef FEAT_DND
static void drop_read_text(GdkDrop *drop, char_u *text);
#endif

/*
 * General purpose callback for reading data from an input stream, either from
 * clipboard or DND.
 */
    static void
read_data_input_cb(
	GInputStream	*stream,
	GAsyncResult	*result,
	ReadData	*rd)
{
    ssize_t r = g_input_stream_read_finish(stream, result, NULL);

    if (r == -1)
    {
	// Error occured
	DropReadData *drd = (DropReadData *)rd;

	if (rd->type == READ_DATA_DROP_DATA)
	    gdk_drop_finish(drd->drop, 0);
    }
    else if (r == 0)
    {
	// EOF, don't need to check for READ_DATA_DROP because that uses GType.
	if (rd->type == READ_DATA_CLIP)
	{
	    ClipReadData    *crd = (ClipReadData *)rd;
	    long	    len;
	    char_u	    *actual, *final;
	    int		    motion_type = MAUTO;
	    char_u	    *tofree = NULL;

	    len = (long)rd->arr->len;
	    actual = final = g_byte_array_free(rd->arr, FALSE);
	    rd->arr = NULL;

	    if (clip_convert_data(&final, &len, &motion_type,
			STRCMP(crd->mime_type, VIM_MIMETYPE_NAME) == 0,
			STRCMP(crd->mime_type, VIMENC_MIMETYPE_NAME) == 0,
			&tofree) == OK)
		clip_yank_selection(motion_type, final, len, crd->cbd);
	    g_free(actual);
	    vim_free(tofree);
	}
#ifdef FEAT_DND
	else if (rd->type == READ_DATA_DROP_DATA)
	{
	    DropReadData    *drd = (DropReadData *)rd;
	    char_u	    *str;

	    // Append NUL
	    g_byte_array_append(rd->arr, (const uint8_t *)"", 1);
	    str = g_byte_array_free(rd->arr, FALSE);
	    rd->arr = NULL;
	    drop_read_text(drd->drop, str);
	    g_free(str);
	    gdk_drop_finish(drd->drop, gdk_drop_get_actions(drd->drop));
	}
#endif
    }
    else
    {
	// Continue reading
	g_byte_array_append(rd->arr, (const uint8_t *)rd->buf, r);
	g_input_stream_read_async(stream, rd->buf, READDATA_BUFSIZE,
		G_PRIORITY_HIGH,
		rd->cancel, (GAsyncReadyCallback)read_data_input_cb, rd);
	return;
    }

    read_data_free(rd);
    g_object_unref(stream);
}

#ifdef FEAT_DND
/*
 * Set up for receiving DND items.
 */
    void
gui_gtk_set_dnd_targets(void)
{
    GdkContentFormatsBuilder *builder = gdk_content_formats_builder_new();

    gdk_content_formats_builder_add_gtype(builder, GDK_TYPE_FILE_LIST);
    gdk_content_formats_builder_add_gtype(builder, G_TYPE_STRING);
    if (clip_html)
	gdk_content_formats_builder_add_mime_type(builder, "text/html");

    gtk_drop_target_async_set_formats(gui.drop_target,
	    gdk_content_formats_builder_free_to_formats(builder));
}

/*
 * Handle textual DND data. Note that this does not finish the drop.
 */
    static void
drop_read_text(GdkDrop *drop, char_u *text)
{
    GdkModifierType state;
    char_u	    dropkey[6] = {
	CSI,
	KS_MODIFIER,
	0,
	CSI,
	KS_EXTRA,
	(char_u)KE_DROP
    };

    if (text == NULL || *text == NUL || !query_pointer_pos(NULL, NULL, &state))
	return;

    dnd_yank_drag_data((char_u *)text, (long)STRLEN(text));

    dropkey[2] = modifiers_gdk2vim(state);
    if (dropkey[2] != 0)
	add_to_input_buf(dropkey, (int)sizeof(dropkey));
    else
	add_to_input_buf(dropkey + 3, (int)(sizeof(dropkey) - 3));
}

    static void
drop_read_value_cb(GdkDrop *drop, GAsyncResult *result, DropReadData *drd)
{
    const GValue    *value = gdk_drop_read_value_finish(drop, result, NULL);
    gboolean	    success = FALSE;

    if (value == NULL)
	goto exit;

    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)
	    goto exit;

	fnames = ALLOC_MULT(char_u *, nfiles);
	if (fnames == NULL)
	    goto exit;

	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(drd->x, drd->y, 0, fnames, nfiles);
	else
	    vim_free(fnames);
	success = TRUE;
    }
    else if (G_VALUE_HOLDS(value, G_TYPE_STRING))
    {
	const char *text = g_value_get_string(value);

	if (text == NULL || *text == NUL)
	    goto exit;

	drop_read_text(drop, (char_u *)text);
	success = TRUE;
    }

exit:
    gdk_drop_finish(drop, success ? gdk_drop_get_actions(drop) : 0);
    read_data_free(&drd->rd);
}

    static void
drop_read_cb(GdkDrop *drop, GAsyncResult *result, DropReadData *drd)
{
    GInputStream    *in_stream;
    const char	    *m;

    in_stream = gdk_drop_read_finish(drop, result, &m, NULL);
    if (in_stream == NULL || STRCMP(m, "text/html") != 0)
    {
	read_data_free(&drd->rd);
	gdk_drop_finish(drop, 0);
	return;
    }

    assert(STRCMP(m, "text/html") == 0);

    // GTK docs says not to use blocking read calls, so do it async.
    g_input_stream_read_async(in_stream, drd->rd.buf, READDATA_BUFSIZE,
	    G_PRIORITY_HIGH,
	    drd->rd.cancel, (GAsyncReadyCallback)read_data_input_cb, drd);
}

// GCancellable of current DND operation, else NULL.
static GCancellable *dnd_cancel = NULL;
static guint dnd_timeout_id = 0;

    static gboolean
drop_timeout_cb(void *udata UNUSED)
{
    g_cancellable_cancel(dnd_cancel);
    g_clear_object(&dnd_cancel);
    dnd_timeout_id = 0;

    return G_SOURCE_REMOVE;
}

/*
 * Drag-and-drop handler for files and text.
 */
    static gboolean
drop_cb(
	GtkDropTargetAsync  *target UNUSED,
	GdkDrop		    *drop,
	double		    x,
	double		    y,
	void		    *udata UNUSED)
{
    GdkContentFormats	*formats = gdk_drop_get_formats(drop);
    DropReadData	*drd = NULL;

    if (gdk_content_formats_contain_gtype(formats, GDK_TYPE_FILE_LIST))
    {
	drd = (DropReadData *)read_data_new(READ_DATA_DROP, sizeof(*drd));
	gdk_drop_read_value_async(drop, GDK_TYPE_FILE_LIST, G_PRIORITY_HIGH,
		drd->rd.cancel, (GAsyncReadyCallback)drop_read_value_cb, drd);
    }
    else if (clip_html
	    && gdk_content_formats_contain_mime_type(formats, "text/html"))
    {
	static const char *m[] = {"text/html", NULL};

	drd = (DropReadData *)read_data_new(READ_DATA_DROP_DATA, sizeof(*drd));
	gdk_drop_read_async(drop, m, G_PRIORITY_HIGH,
		drd->rd.cancel, (GAsyncReadyCallback)drop_read_cb, drd);
    }
    else if (gdk_content_formats_contain_gtype(formats, G_TYPE_STRING))
    {
	drd = (DropReadData *)read_data_new(READ_DATA_DROP, sizeof(*drd));
	gdk_drop_read_value_async(drop, G_TYPE_STRING, G_PRIORITY_HIGH,
		drd->rd.cancel, (GAsyncReadyCallback)drop_read_value_cb, drd);
    }

    if (dnd_cancel != NULL)
	g_cancellable_cancel(dnd_cancel);
    g_clear_object(&dnd_cancel);
    g_clear_handle_id(&dnd_timeout_id, timeout_remove);

    if (drd != NULL)
    {
	drd->drop = g_object_ref(drop);
	drd->x = (int)x;
	drd->y = (int)y;
	dnd_cancel = g_object_ref(drd->rd.cancel);

	// Trying to spin the event loop here causes this GTK assertion error:
	// Gtk:ERROR:../gtk/gtk/gtkdrop.c:70:gtk_drop_begin_event: assertion failed: (self->active == FALSE)
	//
	// This shouldn't be an issue, because Vim DND occurs asynchronously in
	// the first place anyways.

	// Add a 3 second timeout
	dnd_timeout_id = timeout_add(3000, drop_timeout_cb, NULL);

	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);
}

#ifdef FEAT_JOB_CHANNEL
    static timeout_cb_type
channel_poll_cb(gpointer data UNUSED)
{
    // Using an event handler for a channel that may be disconnected does
    // not work, it hangs.  Instead poll for messages.
    channel_handle_events(TRUE);
    parse_queued_messages();
    return TRUE; // Keep repeating
}
#endif

    int
gui_mch_wait_for_chars(long wtime)
{
    int		focus;
    guint	timer;
    static int	timed_out;
    int		retval = FAIL;
#ifdef FEAT_JOB_CHANNEL
    guint	channel_timer = 0;
#endif

    timed_out = FALSE;

    if (wtime >= 0)
	timer = timeout_add(wtime == 0 ? 1L : wtime,
						   input_timer_cb, &timed_out);
    else
	timer = 0;

#ifdef FEAT_JOB_CHANNEL
    // If there is a channel with the keep_open flag we need to poll for input
    // on them.
    if (channel_any_keep_open())
	channel_timer = timeout_add(20, channel_poll_cb, NULL);
#endif

    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);
#ifdef FEAT_JOB_CHANNEL
    if (channel_timer != 0)
	timeout_remove(channel_timer);
#endif

    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));
}

    static int
query_pointer_pos(int *x, int *y, GdkModifierType *state)
{
    GtkNative	    *native;
    GdkSurface	    *surface;
    GdkDisplay	    *display;
    GdkSeat	    *seat;
    GdkDevice	    *pointer;
    double	     sx, sy, nx, ny;
    graphene_point_t src, dst;

    if (gui.drawarea == NULL)
	return FALSE;
    native = gtk_widget_get_native(gui.drawarea);
    if (native == NULL)
	return FALSE;
    surface = gtk_native_get_surface(native);
    if (surface == NULL)
	return FALSE;
    display = gtk_widget_get_display(gui.drawarea);
    if (display == NULL)
	return FALSE;
    seat = gdk_display_get_default_seat(display);
    if (seat == NULL)
	return FALSE;
    pointer = gdk_seat_get_pointer(seat);
    if (pointer == NULL)
	return FALSE;

    if (!gdk_surface_get_device_position(surface, pointer, &sx, &sy, state))
	return FALSE;

    if (x != NULL && y != NULL)
    {
	gtk_native_get_surface_transform(native, &nx, &ny);
	src.x = (float)(sx - nx);
	src.y = (float)(sy - ny);
	if (!gtk_widget_compute_point(GTK_WIDGET(native), gui.drawarea,
		    &src, &dst))
	    return FALSE;

	*x = (int)dst.x;
	*y = (int)dst.y;
    }
    return TRUE;
}

    void
gui_mch_getmouse(int *x, int *y)
{
    if (!query_pointer_pos(x, y, NULL))
    {
	*x = 0;
	*y = 0;
    }
}

    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)
{
}

/*
 * Try getting the actual size of the scrollbar, and update gui.scrollbar_width
 * and gui.scrollbar_height.
 */
    static void
gui_gtk4_update_scrollbar_size(void)
{
    win_T	*wp;
    int		w = -1, h = -1;
    GtkWidget	*sbar;

    FOR_ALL_WINDOWS(wp)
    {
	sbar = wp->w_scrollbars[SBAR_LEFT].id;

	if (sbar == NULL || !gtk_widget_get_visible(sbar)
		|| (!gui.which_scrollbars[SBAR_LEFT]
		    && wp->w_scrollbars[SBAR_RIGHT].id != NULL))
	    sbar = wp->w_scrollbars[SBAR_RIGHT].id;

	if (sbar != NULL && gtk_widget_get_visible(sbar))
	{
	    GtkRequisition  min, nat;
	    int		    sw;

	    // Use preferred size, since widget may not have its size allocated
	    // yet.
	    gtk_widget_get_preferred_size(sbar, &min, &nat);
	    sw = MAX(min.width, nat.width);
	    if (sw > 0)
	    {
		w = sw;
		break;
	    }
	}

    }

    sbar = gui.bottom_sbar.id;
    if (sbar != NULL && gtk_widget_get_visible(sbar))
    {
	GtkRequisition min, nat;
	int		    sh;

	gtk_widget_get_preferred_size(sbar, &min, &nat);
	sh = MAX(min.height, nat.height);

	if (sh > 0)
	    h = sh;
    }

    if (w != -1)
	gui.scrollbar_width = w;
    if (h != -1)
	gui.scrollbar_height = h;
}


/*
 * ============================================================
 * Scrollbar
 * ============================================================
 */

    void
gui_mch_enable_scrollbar(scrollbar_T *sb, int flag)
{
    if (sb->id != NULL)
    {
	gboolean was_visible = gtk_widget_get_visible(sb->id);

	gtk_widget_set_visible(sb->id, flag);

	if (!was_visible && flag)
	    gui_gtk4_update_scrollbar_size();
    }
}

#if defined(FEAT_MENU)
/*
 * ============================================================
 * Menu stubs
 * ============================================================
 */

    void
gui_mch_menu_grey(vimmenu_T *menu, int grey)
{
    if (menu->id == NULL)
	return;
    gtk_widget_set_sensitive(menu->id, !grey);
    gui_mch_update();
}

/*
 * Make menu item hidden or not hidden.
 */
    void
gui_mch_menu_hidden(vimmenu_T *menu, int hidden)
{
    if (menu->id == NULL)
	return;
    gtk_widget_set_visible(menu->id, !hidden);
    gui_mch_update();
}

    void
gui_mch_draw_menubar(void)
{
    // Just make sure that the visual changes get effect immediately
    gui_mch_update();
}
#endif

/*
 * ============================================================
 * Tabline
 * ============================================================
 */

#ifdef FEAT_GUI_TABLINE
    void
gui_mch_show_tabline(int showit)
{
    if (gui.tabline == NULL)
	return;
    gtk_widget_set_visible(gui.tabline, showit);
    gui_mch_update();
}

    int
gui_mch_showing_tabline(void)
{
    return gui.tabline != NULL && gtk_widget_get_visible(gui.tabline);
}

static int ignore_tabline_evt = FALSE;
// Page hovered over in tab line, zero indicates none
static int tabpage_hover = 0;

    static void
tabpage_enter_cb(
	GtkEventController  *controller UNUSED,
	double		    x UNUSED,
	double		    y UNUSED,
	void		    *udata)
{
    tabpage_hover = GPOINTER_TO_INT(udata);
}

    static void
tabline_enter_cb(
	GtkEventController  *controller UNUSED,
	double		    x UNUSED,
	double		    y UNUSED,
	void		    *udata UNUSED)
{
    tabpage_hover = 0;
}

    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)
    {
	GtkNotebookPage	    *page_widget;
	GtkWidget	    *tab_widget;
	GtkEventController  *mcontroller;

	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);
	}

	// Attach motion event controller to detect which tab page is currently
	// hovered over.
	page_widget = gtk_notebook_get_page(GTK_NOTEBOOK(gui.tabline), page);
	mcontroller = gtk_event_controller_motion_new();
	g_signal_connect(mcontroller, "enter", G_CALLBACK(tabpage_enter_cb),
		GINT_TO_POINTER(tab_num));
	gtk_event_controller_set_propagation_phase(
		mcontroller, GTK_PHASE_CAPTURE);
	g_object_get(page_widget, "tab", &tab_widget, NULL);
	gtk_widget_add_controller(tab_widget, mcontroller);

	event_box = gtk_notebook_get_tab_label(GTK_NOTEBOOK(gui.tabline), page);
	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);
}

/*
 * Handle selecting one of the tabs.
 */
    static void
on_select_tab(
	GtkNotebook	*notebook UNUSED,
	gpointer	*page UNUSED,
	gint		idx,
	gpointer	data UNUSED)
{
    if (!ignore_tabline_evt)
	send_tabline_event(idx + 1);
}

/*
 * Handle reordering the tabs (using D&D).
 */
    static void
on_tab_reordered(
	GtkNotebook	*notebook UNUSED,
	gpointer	*page UNUSED,
	gint		idx,
	gpointer	data UNUSED)
{
    if (ignore_tabline_evt)
	return;

    if ((tabpage_index(curtab) - 1) < idx)
	tabpage_move(idx + 1);
    else
	tabpage_move(idx);
}

/*
 * Handle selecting an item in the tab line popup menu.
 */
    static void
tabline_menu_event_cb(VimMenuItem *item, VimMenuItemEvent event, void *udata)
{
    if (event == VIM_MENU_ITEM_CLICKED)
	send_tabline_menu_event(tabpage_hover, GPOINTER_TO_INT(udata));
}

    static void
add_tabline_menu_item(VimMenu *menu, const char *name, int resp)
{
    VimMenuItem *item = VIM_MENU_ITEM(vim_menu_item_new(name,
		tabline_menu_event_cb, GINT_TO_POINTER(resp)));

    vim_menu_insert_item(menu, item, -1);
}

/*
 * Create a menu for the tab line.
 */
    static VimMenu *
create_tabline_popup_menu(void)
{
    VimMenu *menu = VIM_MENU(vim_menu_new());

    add_tabline_menu_item(menu, _("Close Tab"), TABLINE_MENU_CLOSE);
    add_tabline_menu_item(menu, _("New Tab"), TABLINE_MENU_NEW);
    add_tabline_menu_item(menu, _("Open Tab..."), TABLINE_MENU_OPEN);

    return menu;
}

    static void
tabline_menu_press_event(
	GtkGestureClick *gesture,
	int		n_press UNUSED,
	double		x,
	double		y,
	GtkWidget	*popover)
{
    guint but;

    but = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture));
    if (but == GDK_BUTTON_SECONDARY)
    {
	// Right mouse button pressed, popup menu
	GdkRectangle rect;
	rect.x = x;
	rect.y = y;
	rect.width = 1;
	rect.height = 1;

	gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect);
	gtk_popover_popup(GTK_POPOVER(popover));
    }
    else if (but == GDK_BUTTON_PRIMARY)
    {
	if (tabpage_hover == 0)
	    // Click after all tabs moves to next tab page.  When "x" is
	    // small guess it's the left button.
	    send_tabline_event(x < 50 ? -1 : 0);
    }
    else if (but == GDK_BUTTON_MIDDLE)
    {
	if (tabpage_hover != 0)
	    // Middle mouse click on tabpage label closes that tab.
	    send_tabline_menu_event(tabpage_hover, TABLINE_MENU_CLOSE);
    }
}
#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)
{
# ifdef USE_GTK4_SNAPSHOT
    GdkTexture *sign;

    sign = (GdkTexture *)sign_get_image(typenr);
    if (sign == NULL)
	return;

    vim_draw_area_add_sign(VIM_DRAW_AREA(gui.drawarea), sign,
	    row, col, SIGN_WIDTH, SIGN_HEIGHT);
# else
    GdkPixbuf	*sign;
    cairo_t	*cr;
    int		width, height;

    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);
# endif

    gtk_widget_queue_draw(gui.drawarea);
}

    void *
gui_mch_register_sign(char_u *signfile)
{
    if (signfile[0] != NUL && signfile[0] != '-' && gui.in_use)
    {
	GError *error = NULL;
# ifdef USE_GTK4_SNAPSHOT
	GdkTexture  *sign;

	sign = gdk_texture_new_from_filename((const char *)signfile,
							    &error);
	if (sign != NULL)
	    return sign;
# else
	GdkPixbuf *sign;

	sign = gdk_pixbuf_new_from_file((const char *)signfile, &error);
	if (error == NULL)
	    return sign;
# endif

	semsg("E255: %s", error->message);
	g_error_free(error);
    }
    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;
}

#ifndef USE_GTK4_SNAPSHOT
/*
 * 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);
    }
}
#endif

/*
 * 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)
{
    PangoGlyphString	*glyphs;
    int			column_offset = 0;
    int			i;
#ifdef USE_GTK4_SNAPSHOT
    gboolean		s_alloced = FALSE;
#else
    GdkRectangle	area;
    cairo_t		*cr;
#endif

    if (gui.text_context == NULL
#ifndef USE_GTK4_SNAPSHOT
	    || gui.surface == NULL
#endif
	    )
	return len;

#ifndef USE_GTK4_SNAPSHOT
    // Restrict all drawing to the current screen line.
    area.x = gui.border_offset;
    area.y = FILL_Y(row);
    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);
#endif

    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;
	}

#ifdef USE_GTK4_SNAPSHOT
	vim_draw_area_add_glyphs(VIM_DRAW_AREA(gui.drawarea), row, col, len,
		flags, gui.ascii_font, glyphs);
#else
	draw_glyph_string(row, col, len, flags, gui.ascii_font, glyphs, cr);
#endif

	column_offset = len;
    }
    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))
	{
#ifdef USE_GTK4_SNAPSHOT
	    // vim_draw_area_add_glyphs() also handles under decorations. Make
	    // "str" a string of spaces so that under decorations are still
	    // applied.
	    s = g_malloc(len);
	    memset(s, ' ', len);
	    s_alloced = TRUE;
#else
	    column_offset = len;
	    goto skipitall;
#endif
	}

	cluster_width = PANGO_SCALE * gui.char_width;
	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;
		}
	    }

#ifdef USE_GTK4_SNAPSHOT
	    vim_draw_area_add_glyphs(VIM_DRAW_AREA(gui.drawarea),
		    row, col + column_offset, item_cells,
		    flags, item->analysis.font, glyphs);
#else
	    draw_glyph_string(row, col + column_offset, item_cells,
			      flags, item->analysis.font, glyphs, cr);
#endif

	    pango_item_free(item);

	    column_offset += item_cells;
	}

	pango_attr_list_unref(attr_list);
    }

#ifdef USE_GTK4_SNAPSHOT
    if (s_alloced)
	g_free(s);
#else
skipitall:
    draw_under(flags, row, col, column_offset, cr);
    cairo_destroy(cr);
#endif

    pango_glyph_string_free(glyphs);


    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
#ifndef USE_GTK4_SNAPSHOT
	    || gui.surface == NULL
#endif
	    )
	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_MENU)
/*
 * Translate Vim's mnemonic tagging to GTK+ style and convert to UTF-8
 * if necessary.  The caller must vim_free() the returned string.
 *
 *	Input	Output
 *	_	__
 *	&&	&
 *	&	_	stripped if use_mnemonic == FALSE
 *	<Tab>		end of menu label text
 */
    static char_u *
translate_mnemonic_tag(char_u *name, int use_mnemonic)
{
    char_u  *buf;
    char_u  *psrc;
    char_u  *pdest;
    int	    n_underscores = 0;

    name = CONVERT_TO_UTF8(name);
    if (name == NULL)
	return NULL;

    for (psrc = name; *psrc != NUL && *psrc != TAB; ++psrc)
	if (*psrc == '_')
	    ++n_underscores;

    buf = alloc(psrc - name + n_underscores + 1);
    if (buf != NULL)
    {
	pdest = buf;
	for (psrc = name; *psrc != NUL && *psrc != TAB; ++psrc)
	{
	    if (*psrc == '_')
	    {
		*pdest++ = '_';
		*pdest++ = '_';
	    }
	    else if (*psrc != '&')
	    {
		*pdest++ = *psrc;
	    }
	    else if (*(psrc + 1) == '&')
	    {
		*pdest++ = *psrc++;
	    }
	    else if (use_mnemonic)
	    {
		*pdest++ = '_';
	    }
	}
	*pdest = NUL;
    }

    CONVERT_TO_UTF8_FREE(name);
    return buf;
}

/*
 * Enable or disable accelerators for the toplevel menus.
 */
    void
gui_gtk_set_mnemonics(int enable)
{
    vimmenu_T	*menu;
    char_u	*name;

    FOR_ALL_MENUS(menu)
    {
	if (menu->id == NULL)
	    continue;

	name = translate_mnemonic_tag(menu->name, enable);
	// Don't think the check if necessary but still do it anyways
	if (VIM_IS_MENU_BAR_ITEM(menu->id))
	    vim_menu_bar_item_set_text(VIM_MENU_BAR_ITEM(menu->id),
		    (const char *)name);
	vim_free(name);
    }
}

    static void
popupmenu_closed_cb(GtkWidget *popover, void *udata UNUSED)
{
    gtk_widget_unparent(popover);
    if (gui.drawarea != NULL)
	gtk_widget_queue_draw(gui.drawarea);
}

/*
 * Open a popup for the given menu at point (x, y).
 */
    static void
gui_gtk_popup_at(vimmenu_T *menu, int x, int y)
{
    GtkWidget	    *popover;
    GdkRectangle    rect;

    if (menu == NULL || menu->submenu_id == NULL)
	return;

    popover = vim_menu_copy(VIM_MENU(menu->submenu_id));
    gtk_widget_set_parent(popover, gui.drawarea);

    rect.x = x;
    rect.y = y;
    rect.width = rect.height = 1;

    // Make sure popover aligns down-and-to-the-right of the pointer.
    gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
    gtk_widget_set_halign(popover, GTK_ALIGN_START);
    gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect);

    g_signal_connect(GTK_POPOVER(popover), "closed",
	    G_CALLBACK(popupmenu_closed_cb), NULL);
    gtk_popover_popup(GTK_POPOVER(popover));
}

    void
gui_make_popup(char_u *path_name, int mouse_pos)
{
    vimmenu_T	*menu;
    int		x, y;

    menu = gui_find_menu(path_name);
    if (menu == NULL || menu->submenu_id == NULL)
	return;

    if (mouse_pos)
	gui_mch_getmouse(&x, &y);
    else
    {
	x = FILL_X(curwin->w_wincol + curwin->w_wcol + 1) + 1;
	y = FILL_Y(W_WINROW(curwin) + curwin->w_wrow + 1) + 1;
    }

    gui_gtk_popup_at(menu, x, y);

}
#endif // FEAT_MENU

    int
get_menu_tool_width(void)
{
    return 0;
}

    int
get_menu_tool_height(void)
{
    GtkWidget *widgets[] = {
#ifdef FEAT_MENU
	gui.menubar,
#endif
#ifdef FEAT_TOOLBAR
	gui.toolbar,
#endif
#ifdef FEAT_GUI_TABLINE
	gui.tabline
#endif
    };

    int height = 0;

    for (int i = 0; i < ARRAY_LENGTH(widgets); i++)
    {
	GtkRequisition	min;
	GtkRequisition	nat;
	int		h;

	if (widgets[i] == NULL || !gtk_widget_get_visible(widgets[i]))
	    continue;

	h = gtk_widget_get_height(widgets[i]);

	if (h == 0)
	{
	    // Allocation hasn't been updated yet (widget just became visible).
	    // Query the preferred height so the caller gets a valid value
	    // before the layout pass runs.  Use the maximum of minimum and
	    // natural height: GTK may allocate min_h even when natural_h is
	    // smaller (e.g. GtkNotebook tab bar has min_h > natural_h due to
	    // CSS).
	    gtk_widget_get_preferred_size(widgets[i], &min, &nat);
	    height += MAX(min.height, nat.height);
	}
	else
	    height += h;
    }
    return height;
}

/*
 * Update selection targets for regular and primary selections.
 */
    void
gui_gtk_update_selection_formats(Clipboard_T *cbd)
{
    // This will call the ref_formats() vfunc of the content provider, see
    // gui_gtk4_cb.c
    if (cbd == &clip_plus)
	gdk_content_provider_content_changed(gui.regular_provider);
    else
	gdk_content_provider_content_changed(gui.primary_provider);
}

/*
 * Get the GdkClipboard and GdkContentProvider for the given Clipboard_T.
 * clip_star (*) uses PRIMARY, clip_plus (+) uses CLIPBOARD.
 */
    static GdkClipboard *
gtk4_get_clipboard(Clipboard_T *cbd, GdkContentProvider **provider)
{
    GdkDisplay *display;

    if (gui.mainwin == NULL)
	return NULL;

    display = gtk_widget_get_display(gui.mainwin);
    if (display == NULL)
	return NULL;

    if (cbd == &clip_plus)
    {
	if (provider != NULL)
	    *provider = gui.regular_provider;
	return gdk_display_get_clipboard(display);
    }
    else
    {
	if (provider != NULL)
	    *provider = gui.primary_provider;
	return gdk_display_get_primary_clipboard(display);
    }
}

/*
 * Callback for gdk_clipboard_read_async().
 */
    static void
clip_read_cb(GdkClipboard *cb, GAsyncResult *result, ClipReadData *crd)
{
    GInputStream    *in_stream;
    const char	    *mime_type;

    in_stream = gdk_clipboard_read_finish(cb, result, &mime_type, NULL);
    if (in_stream == NULL)
    {
	read_data_free(&crd->rd);
	return;
    }

    // Not sure if we need to copy the string, but do it anyways.
    crd->mime_type = g_strdup(mime_type);
    g_input_stream_read_async(in_stream, crd->rd.buf, READDATA_BUFSIZE,
	    G_PRIORITY_HIGH,
	    crd->rd.cancel, (GAsyncReadyCallback)read_data_input_cb, crd);
}

/*
 * Request the selection from the clipboard.
 */
    void
clip_mch_request_selection(Clipboard_T *cbd)
{
    static const char *mimes_no_html[] = {
	VIMENC_MIMETYPE_NAME,
	VIM_MIMETYPE_NAME,
	"text/plain;charset=utf-8",
	"text/plain",
	NULL
    };

    GdkClipboard *clipboard;
    ClipReadData *crd;
    GCancellable *cancel;

    clipboard = gtk4_get_clipboard(cbd, NULL);
    if (clipboard == NULL)
	return;

    crd = (ClipReadData *)read_data_new(READ_DATA_CLIP, sizeof(*crd));
    crd->cbd = cbd;
    cancel = g_object_ref(crd->rd.cancel);

    gdk_clipboard_read_async(
	    clipboard, clip_html ? supported_mimes : mimes_no_html,
	    G_PRIORITY_HIGH, crd->rd.cancel,
	    (GAsyncReadyCallback)clip_read_cb, crd);

    {
	time_t start;

	// Spin until the done receiving data, with a 3-second wall-clock
	// timeout as a safety net. Use the GCancellable as a way to signal if
	// done (and also use it to cancel if timed out as well).
	start = time(NULL);
	while (!g_cancellable_is_cancelled(cancel)
		&& time(NULL) < start + 3)
	    g_main_context_iteration(NULL, TRUE);

	g_cancellable_cancel(cancel);
	g_object_unref(cancel);
    }
}

static int in_clipboard_set = FALSE;

/*
 * Send the current selection to the clipboard. Do nothing for because we
 * subclass GdkContentProvider which will provide the data only when needed.
 */
    void
clip_mch_set_selection(Clipboard_T *cbd UNUSED)
{
}

    static void
clipboard_changed_cb(GdkClipboard *clipboard, gpointer user_data)
{
    Clipboard_T *cbd = (Clipboard_T *)user_data;

    if (in_clipboard_set)
	return;
    if (gdk_clipboard_is_local(clipboard))
	return;
    clip_lose_selection(cbd);
}

/*
 * Own the selection.
 */
    int
clip_mch_own_selection(Clipboard_T *cbd)
{
    GdkContentProvider	*cp;
    GdkClipboard	*cb = gtk4_get_clipboard(cbd, &cp);
    int			ret;

    if (cb == NULL)
	return FAIL;

    in_clipboard_set = TRUE;
    ret = gdk_clipboard_set_content(cb, cp);
    in_clipboard_set = FALSE;

    return ret ? OK : FAIL;
}

/*
 * 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, NULL);
    if (clipboard == NULL)
	return;

    // Only release ownership if we still own it. We don't want to clear the
    // current selection when we aren't actually the source.
    if (gdk_clipboard_is_local(clipboard))
	gdk_clipboard_set_content(clipboard, NULL);
}

// 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;
}

#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
 * ============================================================
 */

/*
 * 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))
	{
	    GdkTexture *texture = gdk_texture_new_from_filename(
		    (const char *)buf, NULL);

	    if (texture != NULL)
	    {
		image = gtk_image_new_from_paintable(
			GDK_PAINTABLE(texture));
		gtk_widget_set_size_request(image, 24, 24);
		g_object_unref(texture);
	    }
	}
    }

    // 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;
}

    static void
menu_button_clicked_cb(
	VimMenuItem	    *item,
	VimMenuItemEvent    event,
	vimmenu_T	    *menu)
{
    if (event == VIM_MENU_ITEM_CLICKED)
	gui_menu_cb(menu);
    else if (event == VIM_MENU_ITEM_SELECTED)
    {
	// Show tooltip instantly in cmdline message.
	char_u		*tooltip;
	static gboolean did_msg = FALSE;

	if (State & MODE_CMDLINE)
	    return;

	tooltip = CONVERT_TO_UTF8(menu->strings[MENU_INDEX_TIP]);
	if (tooltip != NULL && utf_valid_string(tooltip, NULL))
	{
	    msg((char *)tooltip);
	    did_msg = TRUE;
	    setcursor();
	    out_flush_cursor(TRUE, FALSE);
	}
	else if (did_msg)
	{
	    msg("");
	    did_msg = FALSE;
	    setcursor();
	    out_flush_cursor(TRUE, FALSE);
	}
	CONVERT_TO_UTF8_FREE(tooltip);
    }
}

    void
gui_mch_add_menu(vimmenu_T *menu, int idx)
{
    vimmenu_T	*parent;
    GtkWidget	*parent_widget;
    gboolean	use_mnemonic;
    char_u	*text;

    if (menu->name[0] == ']' || menu_is_popup(menu->name))
    {
	// Attach the popover to drawarea's parent rather than to drawarea
	// itself. GtkDrawingArea is a leaf widget whose snapshot does not
	// iterate children, and parenting a popover to it has been observed to
	// leave the drawing area blank while the popover is open.
	menu->submenu_id = g_object_ref_sink(vim_menu_new());
	gtk_widget_set_parent(menu->submenu_id, gui.drawarea);
	return;
    }

    parent = menu->parent;

    if ((parent != NULL && parent->submenu_id == NULL)
	    || !menu_is_menubar(menu->name))
	return;

    menu->submenu_id = g_object_ref_sink(vim_menu_new());

    use_mnemonic = parent != NULL || p_wak[0] != 'n';
    text = translate_mnemonic_tag(menu->name, use_mnemonic);

    if (parent != NULL)
    {
	parent_widget = parent->submenu_id;
	menu->id = g_object_ref_sink(vim_menu_item_new(
		    (const char *)text, NULL, NULL));

	vim_menu_item_set_submenu(VIM_MENU_ITEM(menu->id),
		VIM_MENU(menu->submenu_id));
	vim_menu_insert_item(VIM_MENU(parent_widget),
		VIM_MENU_ITEM(menu->id), idx);
    }
    else
    {
	parent_widget = gui.menubar;
	menu->id = g_object_ref_sink(vim_menu_bar_item_new(
		    (const char *)text, VIM_MENU(menu->submenu_id)));

	vim_menu_bar_insert_item(VIM_MENU_BAR(parent_widget),
		VIM_MENU_BAR_ITEM(menu->id), idx);
    }

    vim_free(text);
}

    void
gui_mch_add_menu_item(vimmenu_T *menu, int idx)
{
    vimmenu_T	*parent = menu->parent;

#ifdef FEAT_TOOLBAR
    if (parent != NULL && menu_is_toolbar(parent->name))
    {
	if (menu_is_separator(menu->name))
	{
	    menu->id =
		vim_toolbar_insert_separator(VIM_TOOLBAR(gui.toolbar), idx);
	}
	else
	{
	    GtkWidget	*btn;
	    GtkWidget	*icon;
	    char_u	*text;
	    char_u	*tooltip;

	    text    = CONVERT_TO_UTF8(menu->dname);
	    tooltip = CONVERT_TO_UTF8(menu->strings[MENU_INDEX_TIP]);
	    if (tooltip != NULL && !utf_valid_string(tooltip, NULL))
		CONVERT_TO_UTF8_FREE(tooltip);

	    icon = create_toolbar_icon(menu);
	    btn = vim_toolbar_insert_button(VIM_TOOLBAR(gui.toolbar),
		    icon, (const char *)text, idx);
	    gtk_widget_set_tooltip_text(btn, (const gchar *)tooltip);

	    g_signal_connect(btn, "clicked",
		    G_CALLBACK(toolbar_button_clicked_cb), menu);

	    menu->id = btn;
	    CONVERT_TO_UTF8_FREE(text);
	    CONVERT_TO_UTF8_FREE(tooltip);
	}
	return;
    }
#endif

    // Menu items (non-toolbar)
    if (parent == NULL || parent->submenu_id == NULL)
	return;

    if (menu_is_separator(menu->name))
    {
	menu->id = g_object_ref_sink(vim_menu_insert_separator(
		VIM_MENU(parent->submenu_id), idx));
    }
    else
    {
	char_u	    *text;
	char_u	    *accel_text = NULL;
	gboolean    use_mnemonic;

	use_mnemonic = p_wak[0] != 'n';
	text = translate_mnemonic_tag(menu->name, use_mnemonic);

	if (menu->actext != NULL && menu->actext[0] != NUL)
	    accel_text = CONVERT_TO_UTF8(menu->actext);

	// Add our own reference to the widget
	menu->id = g_object_ref_sink(vim_menu_item_new((const char *)text,
		    (VimMenuItemFunc)menu_button_clicked_cb, menu));

	if (accel_text != NULL)
	    vim_menu_item_set_accel(VIM_MENU_ITEM(menu->id),
		    (const char *)accel_text);

	vim_menu_insert_item(VIM_MENU(parent->submenu_id),
		VIM_MENU_ITEM(menu->id), idx);

	vim_free(text);
	CONVERT_TO_UTF8_FREE(accel_text);
    }
}

    void
gui_mch_toggle_tearoffs(int enable UNUSED)
{
    // GTK4: tearoff menus don't exist.
}

    void
gui_mch_menu_set_tip(vimmenu_T *menu)
{
    char_u *tooltip;

    if (menu->id == NULL)
	return;

    tooltip = CONVERT_TO_UTF8(menu->strings[MENU_INDEX_TIP]);
    if (tooltip != NULL && utf_valid_string(tooltip, NULL))
	gtk_widget_set_tooltip_text(menu->id, (const char *)tooltip);
    CONVERT_TO_UTF8_FREE(tooltip);
}

    void
gui_mch_destroy_menu(vimmenu_T *menu)
{
    // For toolbar buttons and separators, remove from the toolbar box.
    if (menu->parent != NULL && menu_is_toolbar(menu->parent->name))
    {
	vim_toolbar_remove(VIM_TOOLBAR(gui.toolbar), menu->id);
	menu->id = NULL;
	return;
    }

    // For popup menus, unparent the menu as well
    if (menu->name[0] == ']' || menu_is_popup(menu->name))
	gtk_widget_unparent(menu->submenu_id);
    else if (menu->parent == NULL)
	// Remove from menubar
	vim_menu_bar_remove(VIM_MENU_BAR(gui.menubar), menu->id);
    else
	// Remove from parent menu
	vim_menu_remove(VIM_MENU(menu->parent->submenu_id), menu->id);

    g_clear_object(&menu->submenu_id);
    g_clear_object(&menu->id);
}

    void
gui_mch_show_popupmenu(vimmenu_T *menu)
{
    int x, y;

    gui_mch_getmouse(&x, &y);
    gui_gtk_popup_at(menu, x, y);
}

    static void
show_menubar_popover(void)
{
    GtkWidget	    *menu;
    GdkRectangle    rect;

    if (gui.menubar == NULL || gui.drawarea == NULL)
	return;

    if (gtk_widget_is_visible(gui.menubar))
    {
	// If menubar is visible, then just show first menu in menubar, like how
	// GTK traditionally seems to do it?
	vim_menu_bar_show(VIM_MENU_BAR(gui.menubar), NULL);
	return;
    }

    // Copy and convert the menubar into a menu popover
    menu = vim_menu_bar_to_menu(VIM_MENU_BAR(gui.menubar));

    gtk_widget_set_parent(menu, gui.drawarea);
    gtk_popover_set_position(GTK_POPOVER(menu), GTK_POS_BOTTOM);
    rect.x = 0;
    rect.y = 0;
    rect.width = 1;
    rect.height = 1;
    gtk_popover_set_pointing_to(GTK_POPOVER(menu), &rect);
    g_signal_connect(menu, "closed", G_CALLBACK(popupmenu_closed_cb), NULL);
    gtk_popover_popup(GTK_POPOVER(menu));
}

/*
 * ============================================================
 * 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_SCROLLBAR(sb->id))
	return;

    adj = gtk_scrollbar_get_adjustment(GTK_SCROLLBAR(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)
	vim_form_move_resize(VIM_FORM(gui.formwin), sb->id, x, y, w, h);
}

    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)
{
    GtkAdjustment *adj;
    if (orient == SBAR_HORIZ)
	sb->id = gtk_scrollbar_new(GTK_ORIENTATION_HORIZONTAL, NULL);
    else
	sb->id = gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL, NULL);

    gtk_widget_add_css_class(sb->id, "vim-scrollbar");
    adj = gtk_scrollbar_get_adjustment(GTK_SCROLLBAR(sb->id));

    gtk_widget_set_visible(sb->id, FALSE);
    vim_form_put(VIM_FORM(gui.formwin), sb->id, 0, 0);

    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)
    {
	vim_form_remove(VIM_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)
{
    // "h" may be negative especially when draw area size is smaller than
    // "gui.char_height".
    if (w <= 0 || h <= 0)
	return;
    last_text_area_w = w;
    last_text_area_h = h;

    vim_form_move_resize(VIM_FORM(gui.formwin), gui.drawarea, x, y, w, h);
}

#ifdef USE_GTK4_SNAPSHOT
/*
 * Calculate the number of pixels to bleed background color to. Should be called
 * after all UI elements are positioned and resized.
 */
    void
gui_gtk_calculate_bleed(int width, int height)
{
    gui.bleed_right = width - last_text_area_w;
    gui.bleed_bot = height - last_text_area_h;

    if (gui.which_scrollbars[SBAR_LEFT])
	gui.bleed_right -= gui.scrollbar_width;
    if (gui.which_scrollbars[SBAR_RIGHT])
	gui.bleed_right -= gui.scrollbar_width;
    if (gui.which_scrollbars[SBAR_BOTTOM])
	gui.bleed_bot -= gui.scrollbar_height;

    // Not sure if this can happen, but be safe...
    if (gui.bleed_right < 0)
	gui.bleed_right = 0;
    if (gui.bleed_bot < 0)
	gui.bleed_bot = 0;
}
#endif

/*
 * ============================================================
 * 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)
{
    GtkFileDialog   *dlg;
    FileDialogData  fdd;
    char_u	    dirbuf[MAXPATHL];
    char_u	    *result = NULL;
    GListStore	    *filters;

    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);
    }

    // Add file filters
    filters = g_list_store_new(GTK_TYPE_FILE_FILTER);
    if (filter != NULL && *filter != NUL)
    {
	int		i = 0;
	char_u		*patt;
	char_u		*p = filter;
	GtkFileFilter	*gfilter;

	gfilter = gtk_file_filter_new();
	patt = alloc(STRLEN(filter));
	if (patt != NULL)
	{
	    while (p != NULL && *p != NUL)
	    {
		if (*p == '\n' || *p == ';' || *p == '\t')
		{
		    STRNCPY(patt, filter, i);
		    patt[i] = '\0';
		    if (*p == '\t')
			gtk_file_filter_set_name(gfilter, (gchar *)patt);
		    else
		    {
			gtk_file_filter_add_pattern(gfilter, (gchar *)patt);
			if (*p == '\n')
			{
			    g_list_store_append(filters, gfilter);
			    g_object_unref(gfilter);
			    if (*(p + 1) != NUL)
				gfilter = gtk_file_filter_new();
			}
		    }
		    filter = ++p;
		    i = 0;
		}
		else
		{
		    p++;
		    i++;
		}
	    }
	}
	vim_free(patt);
    }
    gtk_file_dialog_set_filters(dlg, G_LIST_MODEL(filters));
    g_object_unref(filters);

    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
 * ============================================================
 */

/*
 * Split up button_string into individual button labels by inserting NUL bytes.
 * Also replace the Vim-style mnemonic accelerator prefix '&' with '_'.
 * "button_string" is duplicated; caller must free the duplicated string via
 * *tofree.
 */
    static char **
split_button_string(char_u *button_string, int *n_buttons, char **tofree)
{
    char	    **array;
    char_u	    *p;
    unsigned int    count = 1;

    button_string = (char_u *)g_strdup((const char *)button_string);

    for (p = button_string; *p != NUL; ++p)
	if (*p == DLG_BUTTON_SEP)
	    ++count;

    array = g_malloc_n(count, sizeof(char *));
    count = 0;

    if (array != NULL)
    {
	array[count++] = (char *)button_string;
	for (p = button_string; *p != NUL; )
	{
	    if (*p == DLG_BUTTON_SEP)
	    {
		*p++ = NUL;
		array[count++] = (char *)p;
	    }
	    else if (*p == DLG_HOTKEY_CHAR)
		*p++ = '_';
	    else
		MB_PTR_ADV(p);
	}
    }

    *tofree = (char *)button_string;
    *n_buttons = count;
    return array;
}

/*
 * Convert VIM_GENERIC, VIM_ERROR, etc into an icon name. Returns NULL for
 * VIM_GENERIC.
 */
    static const char *
dialog_type_to_icon(int type)
{
    switch (type)
    {
	case VIM_ERROR:
	    return "dialog-error-symbolic";
	case VIM_WARNING:
	    return "dialog-warning-symbolic";
	case VIM_INFO:
	    return "dialog-information-symbolic";
	case VIM_QUESTION:
	    return "dialog-question-symbolic";
	default:
	    break;
    }
    return NULL;
}

// Data associated with each button in the dialog
typedef struct
{
    int		but_idx;
    int		*response;
    gboolean	*done;
} DialogButtonState;

typedef struct
{
    GtkWidget	*win;
    gboolean	*done;
    gboolean	no_alt;
} DialogState;

    static void
dialog_button_clicked_cb(GtkButton *button, DialogButtonState *state)
{
    *state->response = state->but_idx;
    *state->done = TRUE;
}

    static gboolean
dialog_key_pressed_cb(
	GtkEventControllerKey	*controller,
	guint			keyval,
	guint			keycode,
	GdkModifierType		state,
	DialogState		*dstate)
{
    if (keyval == GDK_KEY_Escape)
    {
	*dstate->done = TRUE;
	return TRUE;
    }

    if (dstate->no_alt && !(state & gtk_accelerator_get_default_mod_mask()))
	return gtk_widget_mnemonic_activate(dstate->win, FALSE);
    return FALSE;
}

    static gboolean
dialog_close_request_cb(GtkWindow *win, gboolean *win_closed)
{
    *win_closed = TRUE;
    return FALSE;
}

    int
gui_mch_dialog(
	int	type,
	char_u	*title,
	char_u	*message,
	char_u	*buttons,
	int	def_but,
	char_u	*textfield,
	int	ex_cmd UNUSED)
{
    GtkWindow		*win = GTK_WINDOW(gtk_window_new());
    GtkWidget		*vertbox;
    GtkWidget		*message_box;
    const char		*icon_name;
    GtkWidget		*icon;
    GtkWidget		*label;
    GtkWidget		*entry = NULL;
    char_u		*utf8_title;
    char_u		*utf8_message;
    GtkEventController *key_controller;
    DialogButtonState	*but_states = NULL;
    char		*tofree = NULL;
    int			response = -1;
    gboolean		done = FALSE;
    gboolean		win_closed = FALSE;
    DialogState		state;

    utf8_title = CONVERT_TO_UTF8(title);
    if (utf8_title != NULL)
	gtk_window_set_title(win, (const char *)utf8_title);
    CONVERT_TO_UTF8_FREE(utf8_title);

    gtk_window_set_transient_for(win, GTK_WINDOW(gui.mainwin));
    gtk_window_set_modal(win, TRUE);
    gtk_window_set_default_size(win, 300, -1);
    gtk_window_set_destroy_with_parent(win, TRUE);
    g_signal_connect(win, "close-request",
	    G_CALLBACK(dialog_close_request_cb), &win_closed);

    // Create main vertical layout container
    vertbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);
    gtk_window_set_child(win, vertbox);
    gtk_widget_set_margin_top(vertbox, 24);
    gtk_widget_set_margin_bottom(vertbox, 24);
    gtk_widget_set_margin_start(vertbox, 12);
    gtk_widget_set_margin_end(vertbox, 12);

    // Add the message label
    message_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
    gtk_widget_set_halign(message_box, GTK_ALIGN_CENTER);
    gtk_box_append(GTK_BOX(vertbox), message_box);

    // If type is not VIM_GENERIC, add an icon to make the dialog look nicer :)
    icon_name = dialog_type_to_icon(type);
    if (icon_name != NULL)
    {
	icon = gtk_image_new_from_icon_name(icon_name);
	gtk_image_set_icon_size(GTK_IMAGE(icon), GTK_ICON_SIZE_LARGE);
	gtk_box_append(GTK_BOX(message_box), icon);
    }

    utf8_message = CONVERT_TO_UTF8(message);
    label = gtk_label_new((const char *)utf8_message);
    CONVERT_TO_UTF8_FREE(utf8_message);
    gtk_label_set_wrap(GTK_LABEL(label), TRUE);
    gtk_label_set_max_width_chars(GTK_LABEL(label), 40);
    gtk_box_append(GTK_BOX(message_box), label);

    // Close the dialog when the <Esc> key is pressed. Also allow using
    // mnemonics without <Alt> key (if there is no text field).
    key_controller = gtk_event_controller_key_new();
    g_signal_connect(key_controller, "key-pressed",
	    G_CALLBACK(dialog_key_pressed_cb), &state);
    gtk_widget_add_controller(GTK_WIDGET(win), key_controller);
    state.done = &done;
    state.win = GTK_WIDGET(win);

    if (textfield != NULL)
    {
	// Add text entry so user can enter text
	char_u *utf8_text = CONVERT_TO_UTF8(textfield);

	entry = gtk_entry_new();

	if (utf8_text != NULL)
	    gtk_editable_set_text(GTK_EDITABLE(entry), (const char *)utf8_text);
	else
	    gtk_editable_set_text(GTK_EDITABLE(entry), "");
	CONVERT_TO_UTF8_FREE(utf8_text);

	// Make it so that pressing enter key will activate "def_but" button
	// (which is set as the default widget).
	gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
	gtk_box_append(GTK_BOX(vertbox), entry);
	state.no_alt = FALSE;
    }
    else
    {
	GListModel  *controllers;
	int	    len;

	// Set all shortcut controllers in the window to not require a modifier
	// for mnemonics.
	controllers = gtk_widget_observe_controllers(GTK_WIDGET(win));
	len  = g_list_model_get_n_items(controllers);
	for (int i = 0; i < len; i++)
	{
	    GtkEventController *controller;

	    controller = g_list_model_get_item(controllers, i);
	    if (GTK_IS_SHORTCUT_CONTROLLER(controller))
		gtk_shortcut_controller_set_mnemonics_modifiers(
			GTK_SHORTCUT_CONTROLLER(controller), 0);
	}
	g_object_unref(controllers);

	gtk_window_set_mnemonics_visible(win, TRUE);
	state.no_alt = TRUE;
    }

    if (buttons != NULL)
    {
	GtkWidget   *but_box;
	char	    **buttons_arr; // Note that array is allocated, not strings
	int	    n_buttons;

	// Check 'v' flag in 'guioptions': vertical button placement.
	if (vim_strchr(p_go, GO_VERTICAL) != NULL)
	    but_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
	else
	    but_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);

	gtk_widget_set_halign(but_box, GTK_ALIGN_CENTER);
	gtk_box_set_homogeneous(GTK_BOX(but_box), TRUE);
	gtk_box_append(GTK_BOX(vertbox), but_box);

	buttons_arr = split_button_string(buttons, &n_buttons, &tofree);

	but_states = g_malloc_n(n_buttons, sizeof(DialogButtonState));

	for (int i = 0; i < n_buttons; i++)
	{
	    char_u		*but_label;
	    GtkWidget		*but;
	    DialogButtonState	*but_state = but_states + i;

	    but_label = CONVERT_TO_UTF8((char_u *)buttons_arr[i]);
	    if (but_label == NULL)
		continue;

	    but = gtk_button_new_with_mnemonic((char *)but_label);
	    if (i == def_but - 1)
		gtk_window_set_default_widget(win, but);
	    gtk_box_append(GTK_BOX(but_box), but);
	    CONVERT_TO_UTF8_FREE(but_label);

	    but_state->but_idx = i;
	    but_state->response = &response;
	    but_state->done = &done;

	    g_signal_connect(but, "clicked",
		    G_CALLBACK(dialog_button_clicked_cb), but_state);
	}
	g_free(buttons_arr);
    }

    gtk_window_present(win);

    while (!done && !win_closed)
	g_main_context_iteration(NULL, TRUE);

    if (done)
    {
	if (textfield != NULL)
	{
	    // Get the text the user entered
	    char_u *text;

	    text = (char_u *)gtk_editable_get_text(GTK_EDITABLE(entry));
	    text = CONVERT_FROM_UTF8(text);
	    vim_strncpy(textfield, text, IOSIZE - 1);
	    CONVERT_FROM_UTF8_FREE(text);
	}

	gtk_window_destroy(win);
    }
    g_free(but_states);
    g_free(tofree);

    // Vim buttons are indexed starting from one.
    return response == -1 ? 0 : response + 1;
}

/*
 * ============================================================
 * 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
    GtkWidget *find;	    // 'Find Next' action button
    GtkWidget *replace;	    // 'Replace With' action button
    GtkWidget *all;	    // 'Replace All' action button
} 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
entry_changed_cb(GtkWidget *entry, GtkWidget *dialog)
{
    const gchar	*entry_text;
    gboolean	nonempty;

    entry_text = gtk_editable_get_text(GTK_EDITABLE(entry));

    if (!entry_text)
	return; // Shouldn't happen

    nonempty = (entry_text[0] != '\0');

    if (dialog == find_widgets.dialog)
	gtk_widget_set_sensitive(find_widgets.find, nonempty);

    if (dialog == repl_widgets.dialog)
    {
	gtk_widget_set_sensitive(repl_widgets.find, nonempty);
	gtk_widget_set_sensitive(repl_widgets.replace, nonempty);
	gtk_widget_set_sensitive(repl_widgets.all, nonempty);
    }
}

    static gboolean
find_key_pressed_cb(
	GtkEventControllerKey	*controller,
	guint			keyval,
	guint			keycode,
	GdkModifierType		state,
	SharedFindReplace	*frdp)
{
    // If the user is holding one of the key modifiers we will just bail out,
    // thus preserving the possibility of normal focus traversal.
    if (state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK))
	return FALSE;

    // the Escape key synthesizes a cancellation action
    if (keyval == GDK_KEY_Escape)
    {
	// Destroy rather than hide the dialog: reusing a hidden toplevel loses
	// window decorations on Wayland
	gtk_window_destroy(GTK_WINDOW(frdp->dialog));
	return TRUE;
    }

    return FALSE;
}

    static void
entry_activate_cb(GtkWidget *widget UNUSED, void *udata)
{
    gtk_widget_grab_focus(GTK_WIDGET(udata));
}

    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		*vertbox, *grid, *hbox, *tmp, *btn;
    gboolean		sensitive;
    GtkEventController	*key_controller;

    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 != NULL)
    {
	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));

	// For :promptfind dialog, always give keyboard focus to 'what' entry.
	// For :promptrepl dialog, give it to 'with' entry if 'what' has a
	// non-empty entry; otherwise, to 'what' entry.
	gtk_widget_grab_focus(frdp->what);
	if (do_replace && g_utf8_strlen(
		    gtk_editable_get_text(GTK_EDITABLE(frdp->what)), -1) > 0)
	    gtk_widget_grab_focus(frdp->with);

	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);

    vertbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
    gtk_widget_set_margin_start(vertbox, 12);
    gtk_widget_set_margin_end(vertbox, 12);
    gtk_widget_set_margin_top(vertbox, 12);
    gtk_widget_set_margin_bottom(vertbox, 12);
    gtk_window_set_child(GTK_WINDOW(frdp->dialog), vertbox);

    // 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(vertbox), 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);
	gtk_entry_set_activates_default(GTK_ENTRY(frdp->with), TRUE);

	// Make the entry activation only change the input focus onto the
	// with item.
	gtk_entry_set_activates_default(GTK_ENTRY(frdp->what), FALSE);
	g_signal_connect(G_OBJECT(frdp->what), "activate",
		G_CALLBACK(entry_activate_cb), frdp->with);
    }
    else
	// Make the entry activation do the search.
	gtk_entry_set_activates_default(GTK_ENTRY(frdp->what), TRUE);

    // Checkboxes
    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
    gtk_box_append(GTK_BOX(vertbox), 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(vertbox), 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(vertbox), hbox);

    btn = gtk_button_new_with_label(_("Find Next"));
    gtk_widget_set_sensitive(btn, sensitive);
    frdp->find = btn;

    key_controller = gtk_event_controller_key_new();
    g_signal_connect(key_controller, "key-pressed",
	    G_CALLBACK(find_key_pressed_cb), frdp);
    gtk_widget_add_controller(GTK_WIDGET(frdp->dialog), key_controller);

    g_signal_connect(G_OBJECT(frdp->what), "changed",
	    G_CALLBACK(entry_changed_cb), frdp->dialog);

    // Make it so that when entry is activated, this button will be activated.
    gtk_window_set_default_widget(GTK_WINDOW(frdp->dialog), btn);
    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"));
	gtk_widget_set_sensitive(btn, sensitive);
	g_signal_connect(btn, "clicked", G_CALLBACK(find_replace_cb),
		GINT_TO_POINTER(FRD_REPLACE));
	frdp->replace = btn;
	gtk_box_append(GTK_BOX(hbox), btn);

	btn = gtk_button_new_with_label(_("Replace All"));
	gtk_widget_set_sensitive(btn, sensitive);
	g_signal_connect(btn, "clicked", G_CALLBACK(find_replace_cb),
		GINT_TO_POINTER(FRD_REPLACEALL));
	frdp->all = btn;
	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);

    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;
    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;

    for (lnum = first; lnum <= last; ++lnum)
    {
	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
