/*
 * mouse.c
 * Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
 *
 * Distributed under terms of the GPL3 license.
 */

#include "state.h"
#include "screen.h"
#include "charsets.h"
#include "dnd.h"
#include <limits.h>
#include <math.h>
#include "glfw-wrapper.h"
#include "control-codes.h"

extern PyTypeObject Screen_Type;

static MouseShape mouse_cursor_shape = TEXT_POINTER;
typedef enum MouseActions { PRESS, RELEASE, DRAG, MOVE, LEAVE } MouseAction;
#define debug debug_input

static void
finish_scroll_animation(Screen *screen) {
    if (screen->callbacks != Py_None) {
        PyObject *ret = PyObject_CallMethod(screen->callbacks, "finish_scroll_animation", NULL);
        if (ret == NULL) PyErr_Print(); else Py_DECREF(ret);
    }
}

// Encoding of mouse events {{{
#define SHIFT_INDICATOR  (1 << 2)
#define ALT_INDICATOR (1 << 3)
#define CONTROL_INDICATOR (1 << 4)
#define MOTION_INDICATOR  (1 << 5)
#define SCROLL_BUTTON_INDICATOR (1 << 6)
#define EXTRA_BUTTON_INDICATOR (1 << 7)
#define LEAVE_INDICATOR (1 << 8)


static unsigned int
button_map(int button) {
    switch(button) {
        case GLFW_MOUSE_BUTTON_LEFT:
            return 1;
        case GLFW_MOUSE_BUTTON_RIGHT:
            return 3;
        case GLFW_MOUSE_BUTTON_MIDDLE:
            return 2;
        case GLFW_MOUSE_BUTTON_4:
        case GLFW_MOUSE_BUTTON_5:
        case GLFW_MOUSE_BUTTON_6:
        case GLFW_MOUSE_BUTTON_7:
        case GLFW_MOUSE_BUTTON_8:
            return button + 5;
        default:
            return UINT_MAX;
    }
}

static unsigned int
encode_button(unsigned int button) {
    if (button >= 8 && button <= 11) {
        return (button - 8) | EXTRA_BUTTON_INDICATOR;
    } else if (button >= 4 && button <= 7) {
        return (button - 4) | SCROLL_BUTTON_INDICATOR;
    } else if (button >= 1 && button <= 3) {
        return button - 1;
    } else {
        return UINT_MAX;
    }
}

static char mouse_event_buf[64];

static int
encode_mouse_event_impl(const MousePosition *mpos, int mouse_tracking_protocol, int button, MouseAction action, int mods) {
    unsigned int cb = encode_button(button);
    switch (action) {
        case MOVE:
            if (cb == UINT_MAX) cb = 3;
            cb += 32;
            break;
        case LEAVE:
            if (mouse_tracking_protocol != SGR_PIXEL_PROTOCOL) return 0;
            cb = LEAVE_INDICATOR | MOTION_INDICATOR;
            break;
        default:
            if (cb == UINT_MAX) return 0;
            break;
    }
    if (action == DRAG || action == MOVE) cb |= MOTION_INDICATOR;
    else if (action == RELEASE && mouse_tracking_protocol < SGR_PROTOCOL) cb = 3;
    if (mods & GLFW_MOD_SHIFT) cb |= SHIFT_INDICATOR;
    if (mods & GLFW_MOD_ALT) cb |= ALT_INDICATOR;
    if (mods & GLFW_MOD_CONTROL) cb |= CONTROL_INDICATOR;
    int x = mpos->cell_x + 1, y = mpos->cell_y + 1;
    switch(mouse_tracking_protocol) {
        case SGR_PIXEL_PROTOCOL:
            x = (int)round(mpos->global_x);
            y = (int)round(mpos->global_y);
            /* fallthrough */
        case SGR_PROTOCOL:
            return snprintf(mouse_event_buf, sizeof(mouse_event_buf), "<%d;%d;%d%s", cb, x, y, action == RELEASE ? "m" : "M");
            break;
        case URXVT_PROTOCOL:
            return snprintf(mouse_event_buf, sizeof(mouse_event_buf), "%d;%d;%dM", cb + 32, x, y);
            break;
        case UTF8_PROTOCOL:
            mouse_event_buf[0] = 'M'; mouse_event_buf[1] = cb + 32;
            unsigned int sz = 2;
            sz += encode_utf8(x + 32, mouse_event_buf + sz);
            sz += encode_utf8(y + 32, mouse_event_buf + sz);
            return sz;
            break;
        default:
            if (x > 223 || y > 223) return 0;
            else {
                mouse_event_buf[0] = 'M'; mouse_event_buf[1] = cb + 32; mouse_event_buf[2] = x + 32; mouse_event_buf[3] = y + 32;
                return 4;
            }
            break;
    }
    return 0;
}

static int
encode_mouse_event(Window *w, int button, MouseAction action, int mods) {
    Screen *screen = w->render_data.screen;
    return encode_mouse_event_impl(&w->mouse_pos, screen->modes.mouse_tracking_protocol, button, action, mods);
}

static int
encode_mouse_button(Window *w, int button, MouseAction action, int mods) {
    if (button == GLFW_MOUSE_BUTTON_LEFT) {
        switch(action) {
            case PRESS:
                global_state.tracked_drag_in_window = w->id;
                global_state.tracked_drag_button = button;
                break;
            case RELEASE:
                global_state.tracked_drag_in_window = 0;
                global_state.tracked_drag_button = -1;
                break;
            default:
                break;
        }
    }
    return encode_mouse_event(w, button_map(button), action, mods);
}

static int
encode_mouse_scroll(Window *w, int button, int mods) {
    return encode_mouse_event(w, button, PRESS, mods);
}

// }}}

static Window*
window_for_id(id_type window_id) {
    if (global_state.callback_os_window && global_state.callback_os_window->num_tabs) {
        Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
        for (unsigned int i = 0; i < t->num_windows; i++) {
            Window *w = t->windows + i;
            if (w->id == window_id) return w;
        }
    }
    return window_for_window_id(window_id);
}

static void
update_scrollbar_hover_state(Window *w, bool hovering) {
    if (!w) return;
    bool changed = w->scrollbar.is_hovering != hovering;
    w->scrollbar.is_hovering = hovering;

    if (changed && global_state.callback_os_window) {
        global_state.callback_os_window->needs_render = true;
        request_tick_callback();
    }
}

static void
set_currently_hovered_window(id_type window_id, int modifiers, bool focus_follows) {
    if (global_state.mouse_hover_in_window != window_id) {
        Window *left_window = window_for_id(global_state.mouse_hover_in_window);
        global_state.mouse_hover_in_window = window_id;
        if (left_window) {
            if (left_window->scrollbar.is_hovering) update_scrollbar_hover_state(left_window, false);
            if (left_window->render_data.screen) screen_mark_url(left_window->render_data.screen, 0, 0, 0, 0);
            if (left_window->drop.hovered) drop_left_child(left_window);
            int sz = encode_mouse_event(left_window, 0, LEAVE, modifiers);
            if (sz > 0) {
                mouse_event_buf[sz] = 0;
                write_escape_code_to_child(left_window->render_data.screen, ESC_CSI, mouse_event_buf);
                debug("Sent mouse leave event to window: %llu currently hovering: %llu\n", left_window->id, window_id);
            }
        }
        if (focus_follows && window_id && OPT(focus_follows_mouse).on_cross && global_state.callback_os_window && global_state.callback_os_window->num_tabs) {
            Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
            for (unsigned i = 0; i < t->num_windows; i++) {
                if (t->windows[i].id == window_id) {
                    if (i != t->active_window) call_boss(switch_focus_to_in_active_tab, "K", window_id);
                    break;
                }
            }
        }
    }
}

static bool
dispatch_mouse_event(Window *w, int button, int count, int modifiers, bool grabbed) {
    bool handled = false;
    if (w->render_data.screen && w->render_data.screen->callbacks != Py_None) {
        PyObject *callback_ret = PyObject_CallMethod(w->render_data.screen->callbacks, "on_mouse_event", "{si si si sO}",
            "button", button, "repeat_count", count, "mods", modifiers, "grabbed", grabbed ? Py_True : Py_False);
        if (callback_ret == NULL) PyErr_Print();
        else {
            handled = callback_ret == Py_True;
            Py_DECREF(callback_ret);
        }
        if (OPT(debug_keyboard)) {
            const char *evname = "move";
            switch(count) {
                case -3: evname = "doubleclick"; break;
                case -2: evname = "click"; break;
                case -1: evname = "release"; break;
                case 1: evname = "press"; break;
                case 2: evname = "doublepress"; break;
                case 3: evname = "triplepress"; break;
            }
            const char *bname = "unknown";
            switch(button) {
                case GLFW_MOUSE_BUTTON_LEFT: bname = "left"; break;
                case GLFW_MOUSE_BUTTON_MIDDLE: bname = "middle"; break;
                case GLFW_MOUSE_BUTTON_RIGHT: bname = "right"; break;
                case GLFW_MOUSE_BUTTON_4: bname = "b4"; break;
                case GLFW_MOUSE_BUTTON_5: bname = "b5"; break;
                case GLFW_MOUSE_BUTTON_6: bname = "b6"; break;
                case GLFW_MOUSE_BUTTON_7: bname = "b7"; break;
                case GLFW_MOUSE_BUTTON_8: bname = "b8"; break;
            }
            debug("\x1b[33mon_mouse_input\x1b[m: %s button: %s %sgrabbed: %d handled_in_kitty: %d\n", evname, bname, format_mods(modifiers), grabbed, handled);
        }
    }
    return handled;
}

static unsigned int
window_left(Window *w) {
    return w->render_data.geometry.left - w->padding.left;
}

static unsigned int
window_right(Window *w) {
    return w->render_data.geometry.right + w->padding.right;
}

static unsigned int
window_top(Window *w) {
    return w->render_data.geometry.top - w->padding.top;
}

static unsigned int
window_bottom(Window *w) {
    return w->render_data.geometry.bottom + w->padding.bottom;
}

static bool
contains_mouse(Window *w) {
    double x = global_state.callback_os_window->mouse_x, y = global_state.callback_os_window->mouse_y;
    return (w->visible && window_left(w) <= x && x < window_right(w) && window_top(w) <= y && y < window_bottom(w));
}

static bool
border_contains_mouse(BorderRect *br, double tolerance, Edge *edges) {
    bool ans = false;
    double x = global_state.callback_os_window->mouse_x, y = global_state.callback_os_window->mouse_y;
    if ((int)br->px.left - tolerance <= x && x < (int)br->px.right + tolerance && (int)br->px.top - tolerance <= y && y < (int)br->px.bottom + tolerance) {
        ans = true;
        if (!br->horizontal) *edges |= br->border_type < 0 ? LEFT_EDGE : RIGHT_EDGE;
        else *edges |= br->border_type < 0 ? TOP_EDGE : BOTTOM_EDGE;
    }
    return ans;
}


static double
distance_to_window(Window *w) {
    double x = global_state.callback_os_window->mouse_x, y = global_state.callback_os_window->mouse_y;
    double cx = (window_left(w) + window_right(w)) / 2.0;
    double cy = (window_top(w) + window_bottom(w)) / 2.0;
    return (x - cx) * (x - cx) + (y - cy) * (y - cy);
}

static bool clamp_to_window = false;

static bool
cell_for_pos(Window *w, unsigned int *x, unsigned int *y, bool *in_left_half_of_cell, OSWindow *os_window) {
    WindowGeometry *g = &w->render_data.geometry;
    Screen *screen = w->render_data.screen;
    if (!screen) return false;
    unsigned int qx = 0, qy = 0;
    bool in_left_half = true;
    double mouse_x = global_state.callback_os_window->mouse_x;
    double mouse_y = global_state.callback_os_window->mouse_y;
    double left = g->left, top = g->top, right = g->right, bottom = g->bottom;
    w->mouse_pos.global_x = mouse_x - left; w->mouse_pos.global_y = mouse_y - top;
    if (clamp_to_window) {
        mouse_x = MIN(MAX(mouse_x, left), right);
        mouse_y = MIN(MAX(mouse_y, top), bottom);
    }
    if (mouse_x < left || mouse_y < top || mouse_x > right || mouse_y > bottom) return false;
    if (mouse_x >= g->right) {
        qx = screen->columns - 1;
        in_left_half = false;
    } else if (mouse_x >= g->left) {
        double xval = (double)(mouse_x - g->left) / os_window->fonts_data->fcm.cell_width;
        double fxval = floor(xval);
        qx = (unsigned int)fxval;
        in_left_half = (xval - fxval <= 0.5) ? true : false;
    }
    if (mouse_y >= g->bottom) qy = screen->lines - 1;
    else if (mouse_y >= g->top) {
        qy = (unsigned int)((double)(mouse_y - g->top - screen->pixel_scroll_offset_y) / os_window->fonts_data->fcm.cell_height);
    }
    if (qx < screen->columns && qy < screen->lines) {
        *x = qx; *y = qy;
        *in_left_half_of_cell = in_left_half;
        return true;
    }
    return false;
}

#define HANDLER(name) static void name(Window UNUSED *w, int UNUSED button, int UNUSED modifiers, unsigned int UNUSED window_idx)

static void
set_mouse_cursor_when_dragging(Screen *screen) {
    MouseShape expected_shape = OPT(pointer_shape_when_dragging);
    if (screen && screen->selections.count && screen->selections.items[0].rectangle_select) expected_shape = OPT(pointer_shape_when_dragging_rectangle);
    if (mouse_cursor_shape != expected_shape) {
        mouse_cursor_shape = expected_shape;
        set_mouse_cursor(mouse_cursor_shape);
    }
}

static void
update_drag(Window *w) {
    Screen *screen = w->render_data.screen;
    if (screen && screen->selections.in_progress) {
        screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, (SelectionUpdate){0});
    }
    set_mouse_cursor_when_dragging(screen);
}

static bool
do_drag_scroll(Window *w, bool upwards) {
    Screen *screen = w->render_data.screen;
    if (screen->linebuf == screen->main_linebuf) {
        finish_scroll_animation(screen);
        screen_history_scroll(screen, SCROLL_LINE, upwards);
        update_drag(w);
        if (mouse_cursor_shape != DEFAULT_POINTER) {
            mouse_cursor_shape = DEFAULT_POINTER;
            set_mouse_cursor(mouse_cursor_shape);
        }
        return true;
    }
    return false;
}

bool
drag_scroll(Window *w, OSWindow *frame) {
    unsigned int margin = frame->fonts_data->fcm.cell_height / 2;
    double y = frame->mouse_y;
    bool upwards = y <= (w->render_data.geometry.top + margin);
    if (upwards || y >= w->render_data.geometry.bottom - margin) {
        if (do_drag_scroll(w, upwards)) {
            frame->last_mouse_activity_at = monotonic();
            return true;
        }
    }
    return false;
}

static void
extend_selection(Window *w, bool ended, bool extend_nearest) {
    Screen *screen = w->render_data.screen;
    if (screen_has_selection(screen)) {
        screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell,
                (SelectionUpdate){.ended=ended, .set_as_nearest_extend=extend_nearest});
    }
}

static void
set_mouse_cursor_for_screen(Screen *screen) {
    MouseShape s = screen_pointer_shape(screen);
    if (s != INVALID_POINTER) {
        mouse_cursor_shape = s;
    } else {
        if (screen->modes.mouse_tracking_mode == NO_TRACKING) {
            mouse_cursor_shape = OPT(default_pointer_shape);
        } else {
            mouse_cursor_shape = OPT(pointer_shape_when_grabbed);
        }
    }
}

static void
handle_mouse_movement_in_kitty(Window *w, int button, bool mouse_cell_changed) {
    Screen *screen = w->render_data.screen;
    if (screen->selections.in_progress && (button == global_state.active_drag_button)) {
        monotonic_t now = monotonic();
        if ((now - w->last_drag_scroll_at) >= ms_to_monotonic_t(20ll) || mouse_cell_changed) {
            update_drag(w);
            w->last_drag_scroll_at = now;
        }
    }

}

static void
detect_url(Screen *screen, unsigned int x, unsigned int y) {
    int hid = screen_detect_url(screen, x, y);
    zero_at_ptr(&screen->current_hyperlink_under_mouse);
    if (hid != 0) {
        mouse_cursor_shape = POINTER_POINTER;
        screen->current_hyperlink_under_mouse.has_detected_url = hid < 0;
        if (hid > 0) {
            screen->current_hyperlink_under_mouse.id = (hyperlink_id_type)hid;
            screen->current_hyperlink_under_mouse.x = x;
            screen->current_hyperlink_under_mouse.y = y;
        }
    } else set_mouse_cursor_for_screen(screen);
}

static bool
should_handle_in_kitty(Window *w, Screen *screen, int button) {
    bool in_tracking_mode = (
        screen->modes.mouse_tracking_mode == ANY_MODE ||
        (screen->modes.mouse_tracking_mode == MOTION_MODE && button >= 0));
    return !in_tracking_mode || global_state.active_drag_in_window == w->id;
}

static bool
set_mouse_position(Window *w, bool *mouse_cell_changed, bool *cell_half_changed) {
    unsigned int x = 0, y = 0;
    bool in_left_half_of_cell = false;
    if (!cell_for_pos(w, &x, &y, &in_left_half_of_cell, global_state.callback_os_window)) return false;
    *mouse_cell_changed = x != w->mouse_pos.cell_x || y != w->mouse_pos.cell_y;
    *cell_half_changed = in_left_half_of_cell != w->mouse_pos.in_left_half_of_cell;
    w->mouse_pos.cell_x = x; w->mouse_pos.cell_y = y;
    w->mouse_pos.in_left_half_of_cell = in_left_half_of_cell;
    return true;
}

// Scrollbar {{{
typedef enum {
    SCROLLBAR_HIT_NONE,
    SCROLLBAR_HIT_TRACK,
    SCROLLBAR_HIT_THUMB
} ScrollbarHitType;

typedef struct {
    double left, right, top, bottom;
    double width, gap, hitbox_expansion;
} ScrollbarGeometry;


static bool
validate_scrollbar_state(const Window *w) {
    return w && w->render_data.screen &&
           w->render_data.screen->historybuf &&
           w->render_data.screen->historybuf->count > 0;
}

static ScrollbarGeometry
calculate_scrollbar_geometry(const Window *w) {
    ScrollbarGeometry geom = {0};
    if (!w || !w->render_data.screen) return geom;

    const WindowGeometry *g = &w->render_data.geometry;
    unsigned cell_width = w->render_data.screen->cell_size.width;
    geom.width = (double)OPT(scrollbar_width) * cell_width;
    if (w->scrollbar.is_hovering) geom.width = (double)OPT(scrollbar_hover_width) * cell_width;
    geom.gap = (double)OPT(scrollbar_gap) * cell_width;
    geom.hitbox_expansion = (double)OPT(scrollbar_hitbox_expansion) * cell_width;

    double right_edge = g->right + g->spaces.right;
    geom.left = right_edge - geom.gap - geom.width - geom.hitbox_expansion;
    geom.right = right_edge + geom.gap;
    geom.top = g->top - g->spaces.top;
    geom.bottom = g->bottom + g->spaces.bottom;

    return geom;
}

static ScrollbarHitType
get_scrollbar_hit_type(const Window *w, double mouse_x, double mouse_y) {
    if (!w || !validate_scrollbar_state(w)) return SCROLLBAR_HIT_NONE;

    ScrollbarGeometry geom = calculate_scrollbar_geometry(w);

    if (mouse_x < geom.left || mouse_x > geom.right ||
        mouse_y < geom.top || mouse_y > geom.bottom) {
        return SCROLLBAR_HIT_NONE;
    }

    OSWindow *os_window = global_state.callback_os_window;
    if (!os_window) return SCROLLBAR_HIT_TRACK;
    double mouse_window_fraction = mouse_y / os_window->viewport_height;
    unsigned cell_width = w->render_data.screen->cell_size.width;
    double hitbox_expansion_fraction = (double)(OPT(scrollbar_hitbox_expansion) * cell_width) / os_window->viewport_height;

    if (mouse_window_fraction >= (w->scrollbar.thumb_top - hitbox_expansion_fraction) &&
        mouse_window_fraction <= (w->scrollbar.thumb_bottom + hitbox_expansion_fraction)) {
        return SCROLLBAR_HIT_THUMB;
    }

    return SCROLLBAR_HIT_TRACK;
}

static void
handle_scrollbar_track_click(Window *w, double mouse_y) {
    if (!w) return;
    Screen *screen = w->render_data.screen;
    if (!validate_scrollbar_state(w)) return;
    finish_scroll_animation(screen);

    if (OPT(scrollbar_jump_on_click)) {
        ScrollbarGeometry geom = calculate_scrollbar_geometry(w);
        double scrollbar_height = geom.bottom - geom.top;
        double mouse_pane_fraction = (mouse_y - geom.top) / scrollbar_height;
        double target_scrolled_by = screen->historybuf->count * (1.0 - mouse_pane_fraction);
        screen_history_scroll_to_absolute(screen, target_scrolled_by);
    } else {
        OSWindow *os_window = global_state.callback_os_window;
        if (!os_window) return;
        double mouse_window_fraction = mouse_y / os_window->viewport_height;
        bool click_above_thumb = mouse_window_fraction < w->scrollbar.thumb_top;
        screen_history_scroll(screen, SCROLL_PAGE, click_above_thumb);
    }
}

static void
end_drag(Window *w) {
    Screen *screen = w->render_data.screen;
    global_state.active_drag_in_window = 0;
    global_state.active_drag_button = -1;
    w->last_drag_scroll_at = 0;
    w->scrollbar.is_dragging = false;

    if (global_state.callback_os_window &&
        get_scrollbar_hit_type(w,
            global_state.callback_os_window->mouse_x,
            global_state.callback_os_window->mouse_y
        ) == SCROLLBAR_HIT_NONE) {
        mouse_cursor_shape = TEXT_POINTER;
        set_mouse_cursor(mouse_cursor_shape);
    }

    if (screen->selections.in_progress) {
        screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, (SelectionUpdate){.ended=true});
    }
}


static void
start_scrollbar_drag(Window *w, double mouse_y) {
    if (!w) return;
    Screen *screen = w->render_data.screen;
    if (!validate_scrollbar_state(w)) return;

    ScrollbarGeometry geom = calculate_scrollbar_geometry(w);
    double scrollbar_height = geom.bottom - geom.top;
    double mouse_pane_fraction = (mouse_y - geom.top) / scrollbar_height;
    w->scrollbar.is_dragging = true;
    w->scrollbar.drag_start_y = mouse_pane_fraction;
    w->scrollbar.drag_start_scrolled_by = screen->scrolled_by;
}

static void
handle_scrollbar_drag(Window *w, double mouse_y) {
    if (!w || !w->scrollbar.is_dragging || !validate_scrollbar_state(w)) return;
    Screen *screen = w->render_data.screen;
    finish_scroll_animation(screen);
    ScrollbarGeometry geom = calculate_scrollbar_geometry(w);
    double scrollbar_height = geom.bottom - geom.top;
    double mouse_pane_fraction = (mouse_y - geom.top) / scrollbar_height;
    double delta_y = mouse_pane_fraction - w->scrollbar.drag_start_y;
    double visible_fraction = (double)screen->lines / (screen->lines + screen->historybuf->count);
    unsigned cell_height = screen->cell_size.height;
    double min_thumb_height_fraction = ((double)OPT(scrollbar_min_handle_height) * cell_height) / scrollbar_height;
    double thumb_height = MAX(min_thumb_height_fraction, visible_fraction);
    double available_space = 1.0 - thumb_height;

    if (available_space > 0) {
        double scroll_fraction = delta_y / available_space;
        double target = w->scrollbar.drag_start_scrolled_by - scroll_fraction * screen->historybuf->count;
        double new_scrolled_by;
        if (target < 0) new_scrolled_by = 0;
        else if (target > screen->historybuf->count) new_scrolled_by = screen->historybuf->count;
        else new_scrolled_by = target;
        screen_history_scroll_to_absolute(screen, new_scrolled_by);
    }
}

static const MouseShape scrollbar_drag_mouse_cursor = NS_RESIZE_POINTER;

static bool
handle_scrollbar_mouse(Window *w, int button, MouseAction action, int modifiers UNUSED) {
    if (!w || !OPT(scrollbar_interactive) || !global_state.callback_os_window) return false;

    double mouse_x = global_state.callback_os_window->mouse_x;
    double mouse_y = global_state.callback_os_window->mouse_y;

    if (action == MOVE && w->scrollbar.is_dragging) {
        handle_scrollbar_drag(w, mouse_y);
        mouse_cursor_shape = scrollbar_drag_mouse_cursor;
        set_mouse_cursor(mouse_cursor_shape);
        return true;
    }
    if (global_state.active_drag_in_window == w->id || global_state.tracked_drag_in_window == w->id) return false;

    ScrollbarHitType hit_type = get_scrollbar_hit_type(w, mouse_x, mouse_y);
    bool hovering = (hit_type != SCROLLBAR_HIT_NONE);
    update_scrollbar_hover_state(w, hovering);

    if (!hovering) return false;

    mouse_cursor_shape = DEFAULT_POINTER;
    set_mouse_cursor(mouse_cursor_shape);

    if (button == GLFW_MOUSE_BUTTON_LEFT && action != MOVE) {
        bool is_release = (action == RELEASE);

        if (is_release) {
            if (w->scrollbar.is_dragging) {
                end_drag(w);
            } else if (hit_type == SCROLLBAR_HIT_TRACK) {
                handle_scrollbar_track_click(w, mouse_y);
            }
        } else {
            if (hit_type == SCROLLBAR_HIT_THUMB) {
                start_scrollbar_drag(w, mouse_y);
                global_state.active_drag_in_window = w->id;
                global_state.active_drag_button = button;
            } else if (hit_type == SCROLLBAR_HIT_TRACK && OPT(scrollbar_jump_on_click)) {
                handle_scrollbar_track_click(w, mouse_y);
                start_scrollbar_drag(w, mouse_y);
                global_state.active_drag_in_window = w->id;
                global_state.active_drag_button = button;
            }
        }
    }

    return true;
}
// }}}

static double
distance(double x1, double y1, double x2, double y2) {
    return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

HANDLER(handle_move_event) {
    modifiers &= ~GLFW_LOCK_MASK;

    if (handle_scrollbar_mouse(w, -1, MOVE, modifiers)) return;

    bool mouse_cell_changed = false;
    bool cell_half_changed = false;
    if (!set_mouse_position(w, &mouse_cell_changed, &cell_half_changed)) {
        if (w->scrollbar.is_hovering) {
            update_scrollbar_hover_state(w, false);
        }
        return;
    }
    Screen *screen = w->render_data.screen;
    if (OPT(detect_urls)) detect_url(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y);
    if (should_handle_in_kitty(w, screen, button)) {
        handle_mouse_movement_in_kitty(w, button, mouse_cell_changed | cell_half_changed);
    } else {
        if (mouse_cell_changed || screen->modes.mouse_tracking_protocol == SGR_PIXEL_PROTOCOL) {
            int sz = encode_mouse_button(w, button, button >=0 ? DRAG : MOVE, modifiers);
            if (sz > 0) { mouse_event_buf[sz] = 0; write_escape_code_to_child(screen, ESC_CSI, mouse_event_buf); }
        }
    }
    if (w->drag_source.initial_left_press.at && distance(w->mouse_pos.global_x, w->mouse_pos.global_y, w->drag_source.initial_left_press.x, w->drag_source.initial_left_press.y) > OPT(drag_threshold)) {
        zero_at_ptr(&w->drag_source.initial_left_press);
        if (w->drag_source.can_offer) {
            drag_offer_start_to_child(
                w, w->mouse_pos.cell_x, w->mouse_pos.cell_y, (int)w->mouse_pos.global_x, (int)w->mouse_pos.global_y);
            debug("Sent drag start event to child\n");
        } else if (w->drag_source.potential_url_drag.active) {
            w->drag_source.potential_url_drag.active = false;
            screen_detect_url(screen, w->drag_source.potential_url_drag.x, w->drag_source.potential_url_drag.y);
            if (screen->current_hyperlink_under_mouse.id || screen->current_hyperlink_under_mouse.has_detected_url) {
                screen_open_url(screen, "drag_url");
                debug("Started URL drag\n");
            }
        }
    }
}

static void
clear_click_queue(Window *w, int button) {
    if (0 <= button && button <= (ssize_t)arraysz(w->click_queues)) w->click_queues[button].length = 0;
}

#define N(n) (q->clicks[q->length - n])

static double
radius_for_multiclick(void) {
    return 0.5 * (global_state.callback_os_window ? global_state.callback_os_window->fonts_data->fcm.cell_height : 8);
}

static bool
release_is_click(const Window *w, int button) {
    const ClickQueue *q = &w->click_queues[button];
    monotonic_t now = monotonic();
    return (q->length > 0 && distance(N(1).x, N(1).y, MAX(0, w->mouse_pos.global_x), MAX(0, w->mouse_pos.global_y)) <= radius_for_multiclick() && now - N(1).at < OPT(click_interval));
}

static unsigned
multi_click_count(const Window *w, int button) {
    const ClickQueue *q = &w->click_queues[button];
    double multi_click_allowed_radius = radius_for_multiclick();
    if (q->length > 2) {
        // possible triple-click
        if (
                N(1).at - N(3).at <= 2 * OPT(click_interval) &&
                distance(N(1).x, N(1).y, N(3).x, N(3).y) <= multi_click_allowed_radius
           ) return 3;
    }
    if (q->length > 1) {
        // possible double-click
        if (
                N(1).at - N(2).at <= OPT(click_interval) &&
                distance(N(1).x, N(1).y, N(2).x, N(2).y) <= multi_click_allowed_radius
           ) return 2;
    }
    return q->length ? 1 : 0;
}


static void
add_press(Window *w, int button, int modifiers) {
    if (button < 0 || button >= (ssize_t)arraysz(w->click_queues)) return;
    modifiers &= ~GLFW_LOCK_MASK;
    ClickQueue *q = &w->click_queues[button];
    if (q->length == CLICK_QUEUE_SZ) { memmove(q->clicks, q->clicks + 1, sizeof(Click) * (CLICK_QUEUE_SZ - 1)); q->length--; }
    monotonic_t now = monotonic();
    static unsigned long num = 0;
    N(0).at = now; N(0).button = button; N(0).modifiers = modifiers; N(0).x = MAX(0, w->mouse_pos.global_x); N(0).y = MAX(0, w->mouse_pos.global_y); N(0).num = ++num;
    q->length++;
    Screen *screen = w->render_data.screen;
    int count = multi_click_count(w, button);
    if (count > 1) {
        if (screen) dispatch_mouse_event(w, button, count, modifiers, screen->modes.mouse_tracking_mode != 0);
        if (count > 2) q->length = 0;
    }
}
#undef N

bool
mouse_open_url(Window *w) {
    Screen *screen = w->render_data.screen;
    detect_url(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y);
    return screen_open_url(screen, "open_url");
}

bool
mouse_set_last_visited_cmd_output(Window *w) {
    Screen *screen = w->render_data.screen;
    return screen_set_last_visited_prompt(screen, w->mouse_pos.cell_y);
}

bool
mouse_select_cmd_output(Window *w) {
    Screen *screen = w->render_data.screen;
    return screen_select_cmd_output(screen, w->mouse_pos.cell_y);
}

bool
move_cursor_to_mouse_if_at_shell_prompt(Window *w) {
    Screen *screen = w->render_data.screen;
    int y = screen_cursor_at_a_shell_prompt(screen);
    if (y < 0 || (unsigned)y > w->mouse_pos.cell_y) return false;

    bool is_relative;
    if (screen_prompt_supports_click_events(screen, &is_relative)) {
        MousePosition mpos = w->mouse_pos;
        if (is_relative) mpos.cell_y -= y;
        int sz = encode_mouse_event_impl(&mpos, SGR_PROTOCOL, 1, PRESS, 0);
        if (sz > 0) {
            mouse_event_buf[sz] = 0;
            write_escape_code_to_child(screen, ESC_CSI, mouse_event_buf);
            return true;
        }
        return false;
    } else {
        return screen_fake_move_cursor_to_position(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y);
    }
}


void
send_pending_click_to_window(Window *w, int i) {
    const id_type wid = w->id;
    if (i < 0) {
        while(true) {
            w = window_for_id(wid);
            if (!w || !w->pending_clicks.num) break;
            send_pending_click_to_window(w, w->pending_clicks.num - 1);
        }
        return;
    }
    PendingClick pc = w->pending_clicks.clicks[i];
    remove_i_from_array(w->pending_clicks.clicks, (unsigned)i, w->pending_clicks.num);
    const ClickQueue *q = &w->click_queues[pc.button];
    // only send click if no presses have happened since the release that triggered
    // the click or if the subsequent press is too far or too late for a double click
    if (!q->length) return;
#define press(n) q->clicks[q->length - n]
    if (
            press(1).at <= pc.at || // latest press is before click release
            (q->length > 1 && press(2).num == pc.press_num &&  (   // penultimate press is the press that belongs to this click
                press(1).at - press(2).at > OPT(click_interval) ||  // too long between the presses for it to be a double click
                distance(press(1).x, press(1).y, press(2).x, press(2).y) > pc.radius_for_multiclick  // presses are too far apart
            ))
    ) {
        MousePosition current_pos = w->mouse_pos;
        w->mouse_pos = pc.mouse_pos;
        dispatch_mouse_event(w, pc.button, pc.count, pc.modifiers, pc.grabbed);
        w = window_for_id(wid);
        if (w) w->mouse_pos = current_pos;
    }
#undef press
}

static void
dispatch_possible_click(Window *w, int button, int modifiers) {
    Screen *screen = w->render_data.screen;
    int count = multi_click_count(w, button);
    if (release_is_click(w, button)) {
        ensure_space_for(&(w->pending_clicks), clicks, PendingClick, w->pending_clicks.num + 1, capacity, 4, true);
        PendingClick *pc = w->pending_clicks.clicks + w->pending_clicks.num++;
        zero_at_ptr(pc);
        const ClickQueue *q = &w->click_queues[button];
        pc->press_num = q->length ? q->clicks[q->length - 1].num : 0;
        pc->window_id = w->id;
        pc->mouse_pos = w->mouse_pos;
        pc->at = monotonic();
        pc->button = button;
        pc->count = count == 2 ? -3 : -2;
        pc->modifiers = modifiers;
        pc->grabbed = screen->modes.mouse_tracking_mode != 0;
        pc->radius_for_multiclick = radius_for_multiclick();
        add_main_loop_timer(OPT(click_interval), false, dispatch_pending_clicks, NULL, NULL);
    }
}

HANDLER(handle_button_event) {
    modifiers &= ~GLFW_LOCK_MASK;
    OSWindow *osw = global_state.callback_os_window;
    if (!osw) return;
    w->drag_source.potential_url_drag.active = false;

    Tab *t = osw->tabs + osw->active_tab;
    bool is_release = !osw->mouse_button_pressed[button];

    if (button == GLFW_MOUSE_BUTTON_LEFT && osw->suppress_left_mouse_release) {
        osw->suppress_left_mouse_release = false;
        if (is_release) {
            zero_at_ptr(&w->drag_source.initial_left_press);
            return;
        }
    }

    if (handle_scrollbar_mouse(w, button, is_release ? RELEASE : PRESS, modifiers)) return;

    bool suppress_child_forwarding = false;
    if (osw->is_focused && window_idx != t->active_window && !is_release) {
        call_boss(switch_focus_to_in_active_tab, "K", t->windows[window_idx].id);
        if (button == GLFW_MOUSE_BUTTON_LEFT) {
            // Treat split-focus transfer clicks as focus-only for child processes:
            // suppress forwarding the left press and matching release to the child
            // to avoid release-without-press reports. Still allow kitty to process
            // the event internally (e.g., start text selection via click-and-drag).
            osw->suppress_left_mouse_release = true;
            suppress_child_forwarding = true;
        }
    }

    Screen *screen = w->render_data.screen;
    if (!screen) return;

    bool a, b;
    if (!set_mouse_position(w, &a, &b)) return;
    if (button == GLFW_MOUSE_BUTTON_LEFT) {
        if (is_release) {
            zero_at_ptr(&w->drag_source.initial_left_press);
        } else {
            w->drag_source.initial_left_press.x = w->mouse_pos.global_x;
            w->drag_source.initial_left_press.y = w->mouse_pos.global_y;
            w->drag_source.initial_left_press.at = monotonic();
        }
    }
    id_type wid = w->id;
    if (!dispatch_mouse_event(w, button, is_release ? -1 : 1, modifiers, screen->modes.mouse_tracking_mode != 0)) {
        if (!suppress_child_forwarding && screen->modes.mouse_tracking_mode != 0) {
            int sz = encode_mouse_button(w, button, is_release ? RELEASE : PRESS, modifiers);
            if (sz > 0) { mouse_event_buf[sz] = 0; write_escape_code_to_child(screen, ESC_CSI, mouse_event_buf); }
        }
    }
    // the windows array might have been re-alloced in dispatch_mouse_event
    w = NULL;
    for (size_t i = 0; i < t->num_windows && !w; i++) if (t->windows[i].id == wid) w = t->windows + i;
    if (w) {
        if (is_release) dispatch_possible_click(w, button, modifiers);
        else add_press(w, button, modifiers);
    }
}

static int
currently_pressed_button(void) {
    for (int i = 0; i <= GLFW_MOUSE_BUTTON_LAST; i++) {
        if (global_state.callback_os_window->mouse_button_pressed[i]) return i;
    }
    return -1;
}

HANDLER(handle_event) {
    modifiers &= ~GLFW_LOCK_MASK;
    set_mouse_cursor_for_screen(w->render_data.screen);
    set_currently_hovered_window(w->id, modifiers, true);
    if (button == -1) {
        button = currently_pressed_button();
        handle_move_event(w, button, modifiers, window_idx);
    } else {
        handle_button_event(w, button, modifiers, window_idx);
    }
}

static void
handle_window_title_bar_mouse(Window *w, int button, int modifiers, int action) {
    OSWindow *osw = global_state.callback_os_window;
    if (!osw) return;
    if (button > -1 || global_state.window_being_dragged.id) {
        call_boss(handle_window_title_bar_mouse, "KKddiii",
            osw->id, w->id, osw->mouse_x, osw->mouse_y, button, modifiers, action);
    }
}

static void
handle_tab_bar_mouse(int button, int modifiers, int action) {
    set_currently_hovered_window(0, modifiers, false);
    OSWindow *w = global_state.callback_os_window;
    if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_RELEASE && global_state.tab_being_dragged.id
            && global_state.tab_being_dragged.drag_started && !global_state.drag_source.is_active) {
        // Once a system drag and drop is active the release is consumed by it
        // and never delivered to us, so getting one here means the drag never
        // became a system DND: either glfwStartDrag failed/was not called yet
        // or the compositor silently ignored it (Wayland with a stale serial).
        // Clear the drag state so mouse handling is not redirected to the tab
        // bar forever, and swallow the release as it ended an aborted drag.
        zero_at_ptr(&global_state.tab_being_dragged);
        // re-render the tab bar in case it was drawn without the dragged tab
        if (w) w->tab_bar_data_updated = false;
        return;
    }
    // dont report motion events, as they are expensive and useless
    if (w && (button > -1 || global_state.tab_being_dragged.id)) {
        call_boss(handle_tab_bar_mouse, "Kddiii", w->id, w->mouse_x, w->mouse_y, button, modifiers, action);
    }
}

static bool
mouse_in_region(Region *r) {
    if (r->left == r->right) return false;
    OSWindow *w = global_state.callback_os_window;
    if (!w) return false;
    if (w->mouse_y < r->top || w->mouse_y >= r->bottom) return false;
    if (w->mouse_x < r->left || w->mouse_x >= r->right) return false;
    return true;
}

static unsigned
num_visible_windows(Tab *t) {
    unsigned ans = t->num_windows;
    for (unsigned i = 0; i < t->num_windows; i++) if (!t->windows[i].visible) ans--;
    return ans;
}

typedef struct MouseRegion {
    unsigned window_idx;
    bool in_tab_bar;
    bool in_title_bar;
    Edge window_border;
    Window *window;
} MouseRegion;

static MouseRegion
mouse_region(bool detect_borders, bool detect_title_bar) {
    MouseRegion ans = {0};
    Region central, tab_bar;
    const OSWindow* w = global_state.callback_os_window;
    os_window_regions(w, &central, &tab_bar);
    const bool in_central = mouse_in_region(&central);
    if (!in_central) {
        if (
                (tab_bar.top < central.top && w->mouse_y < central.top) ||
                (tab_bar.bottom > central.bottom && w->mouse_y >= central.bottom)
           ) ans.in_tab_bar = true;
    }
    if (in_central && w->num_tabs > 0) {
        Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
        if (detect_borders && num_visible_windows(t) > 1) {
            id_type window_id = 0;
            double dpi = (w->fonts_data->logical_dpi_x + w->fonts_data->logical_dpi_y) / 2.;
            double tolerance = ((long)round((OPT(window_drag_tolerance) * (dpi / 72.0))));
            BorderRect *closest_vert = NULL, *closest_horiz = NULL;
            double closest_vert_dist = (double)UINT_MAX, closest_horiz_dist = (double)UINT_MAX;
            bool is_within_border_without_tolerance = false;
            for (unsigned i = 0; i < t->border_rects.num_border_rects; i++) {
                BorderRect *br = t->border_rects.rect_buf + i;
                if (!br->border_type) continue;
                Edge edges = 0;
                if (border_contains_mouse(br, 0, &edges)) {
                    ans.window_border |= edges;
                    is_within_border_without_tolerance = true;
                    if (edges & (LEFT_EDGE | RIGHT_EDGE)) { closest_vert_dist = -1; closest_vert = NULL; }
                    else { closest_horiz_dist = -1; closest_horiz = NULL; }
                    window_id = br->border_type < 0 ? -br->border_type : br->border_type;
                } else if (border_contains_mouse(br, tolerance, &edges)) {
                    unsigned width = br->px.right - br->px.left, height = br->px.bottom - br->px.top;
                    if (!br->horizontal) {
                        double d = br->px.left + width/2. - w->mouse_x; d = d*d;
                        if (d < closest_vert_dist) { closest_vert_dist = d; closest_vert = br; }
                    } else {
                        double d = br->px.top + height/2. - w->mouse_y; d = d*d;
                        if (d < closest_horiz_dist) { closest_horiz_dist = d; closest_horiz = br; }
                    }
                }
            }
            if (!is_within_border_without_tolerance) {
                if (closest_vert && border_contains_mouse(closest_vert, tolerance, &ans.window_border) && !window_id)
                    window_id = closest_vert->border_type < 0 ? -closest_vert->border_type : closest_vert->border_type;
                if (closest_horiz && border_contains_mouse(closest_horiz, tolerance, &ans.window_border) && !window_id)
                    window_id = closest_horiz->border_type < 0 ? -closest_horiz->border_type : closest_horiz->border_type;
            }
            if (ans.window_border) {
                // If a hovered scrollbar overlaps this border region, the
                // scrollbar takes precedence to avoid flickering when the
                // expanded hover-width scrollbar straddles the border.
                bool scrollbar_takes_precedence = false;
                if (OPT(scrollbar_interactive)) {
                    for (unsigned int i = 0; i < t->num_windows; i++) {
                        Window *win = t->windows + i;
                        if (!win->visible || !win->render_data.screen) continue;
                        if (win->scrollbar.is_hovering && get_scrollbar_hit_type(win, w->mouse_x, w->mouse_y) != SCROLLBAR_HIT_NONE) {
                            scrollbar_takes_precedence = true;
                            break;
                        }
                    }
                }
                if (!scrollbar_takes_precedence) {
                    if (window_id) {
                        for (unsigned int i = 0; i < t->num_windows; i++)
                            if (t->windows[i].id == window_id) {
                                ans.window = t->windows + i;
                                ans.window_idx = i;
                                break;
                            }
                    }
                    return ans;
                }
                ans.window_border = 0;
            }
        }
        for (unsigned int i = 0; i < t->num_windows; i++) {
            Window *win = t->windows + i;
            if (contains_mouse(win) && win->render_data.screen) {
                ans.window_idx = i; ans.window = win; break;
            } else if (detect_title_bar && win->visible) {
                const WindowRenderData *trd = &win->window_title_render_data;
                if (trd->screen && trd->geometry.right > trd->geometry.left && trd->geometry.bottom > trd->geometry.top) {
                    if (w->mouse_x >= trd->geometry.left && w->mouse_x < trd->geometry.right &&
                            w->mouse_y >= trd->geometry.top && w->mouse_y < trd->geometry.bottom) {
                        ans.in_title_bar = true; ans.window = win; ans.window_idx = i;
                        break;
                    }
                }
            }
        }
        // If no window was found via contains_mouse, check if the mouse is in any
        // window's scrollbar hit area. The scrollbar may be drawn in the margin
        // which is outside the area covered by contains_mouse.
        if (!ans.window && OPT(scrollbar_interactive)) {
            for (unsigned int i = 0; i < t->num_windows; i++) {
                Window *win = t->windows + i;
                if (!win->visible || !win->render_data.screen) continue;
                if (get_scrollbar_hit_type(win, w->mouse_x, w->mouse_y) != SCROLLBAR_HIT_NONE) {
                    ans.window_idx = i; ans.window = win; break;
                }
            }
        }
    }
    return ans;
}

static Window*
closest_window_for_event(unsigned int *window_idx) {
    Window *ans = NULL;
    double closest_distance = UINT_MAX;
    if (global_state.callback_os_window->num_tabs > 0) {
        Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
        for (unsigned int i = 0; i < t->num_windows; i++) {
            Window *w = t->windows + i;
            if (w->visible) {
                double d = distance_to_window(w);
                if (d < closest_distance) { ans = w; closest_distance = d; *window_idx = i; }
            }
        }
    }
    return ans;
}

void
focus_in_event(void) {
    // Ensure that no URL is highlighted and the mouse cursor is in default shape
    mouse_cursor_shape = TEXT_POINTER;
    MouseRegion r = mouse_region(false, false);
    if (r.window && r.window->render_data.screen) {
        screen_mark_url(r.window->render_data.screen, 0, 0, 0, 0);
        set_mouse_cursor_for_screen(r.window->render_data.screen);
    }
    set_mouse_cursor(mouse_cursor_shape);
}

void
update_mouse_pointer_shape(void) {
    mouse_cursor_shape = TEXT_POINTER;
    MouseRegion r = mouse_region(false, true);
    if (r.in_tab_bar) {
        mouse_cursor_shape = POINTER_POINTER;
    } else if (r.in_title_bar) {
        mouse_cursor_shape = POINTER_POINTER;
    } else if (r.window) {
        if (handle_scrollbar_mouse(r.window, -1, MOVE, 0)) {
            mouse_cursor_shape = scrollbar_drag_mouse_cursor;
        } else if (r.window->render_data.screen) {
            screen_mark_url(r.window->render_data.screen, 0, 0, 0, 0);
            set_mouse_cursor_for_screen(r.window->render_data.screen);
        }
    }
    set_mouse_cursor(mouse_cursor_shape);
}

void
leave_event(int modifiers) {
    if (global_state.redirect_mouse_handling || global_state.active_drag_in_window || global_state.tracked_drag_in_window) return;
    set_currently_hovered_window(0, modifiers, false);
}

void
enter_event(int modifiers, bool cursor_moved) {
#ifdef __APPLE__
    // On cocoa there is no way to configure the window manager to
    // focus windows on mouse enter, so we do it ourselves
    if (OPT(focus_follows_mouse).on_cross && !global_state.callback_os_window->is_focused) {
        id_type wid = global_state.callback_os_window->id;
        focus_os_window(global_state.callback_os_window, false, NULL);
        if (!global_state.callback_os_window) {
            global_state.callback_os_window = os_window_for_id(wid);
            if (!global_state.callback_os_window) return;
        }
    }
#endif
    // If the mouse is grabbed send a move event to update the cursor position
    // since the last report.
    if (global_state.redirect_mouse_handling || global_state.active_drag_in_window || global_state.tracked_drag_in_window) return;
    MouseRegion r = mouse_region(false, false);
    Window *w = r.window;
    set_currently_hovered_window(w ? w->id : 0, modifiers, cursor_moved);
    if (!w || r.in_tab_bar || r.in_title_bar) return;

    if (handle_scrollbar_mouse(w, -1, MOVE, modifiers)) return;

    bool mouse_cell_changed = false, cell_half_changed = false;
    if (!set_mouse_position(w, &mouse_cell_changed, &cell_half_changed)) return;
    Screen *screen = w->render_data.screen;
    int button = currently_pressed_button();
    if (!screen || should_handle_in_kitty(w, screen, button)) return;
    int sz = encode_mouse_button(w, button, button >=0 ? DRAG : MOVE, modifiers);
    if (sz > 0) { mouse_event_buf[sz] = 0; write_escape_code_to_child(screen, ESC_CSI, mouse_event_buf); }
}


typedef enum MouseSelectionType {
    MOUSE_SELECTION_NORMAL,
    MOUSE_SELECTION_EXTEND,
    MOUSE_SELECTION_RECTANGLE,
    MOUSE_SELECTION_WORD,
    MOUSE_SELECTION_LINE,
    MOUSE_SELECTION_LINE_FROM_BEGIN,
    MOUSE_SELECTION_LINE_FROM_POINT,
    MOUSE_SELECTION_WORD_AND_LINE_FROM_POINT,
    MOUSE_SELECTION_MOVE_END,
    MOUSE_SELECTION_UPTO_SURROUNDING_WHITESPACE,
} MouseSelectionType;


void
mouse_selection(Window *w, int code, int button) {
    global_state.active_drag_in_window = w->id;
    global_state.active_drag_button = button;
    Screen *screen = w->render_data.screen;
    index_type start, end;
    unsigned int y1, y2;
#define S(mode) {\
        screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, mode); \
        screen_update_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, (SelectionUpdate){.start_extended_selection=true}); }

    switch((MouseSelectionType)code) {
        case MOUSE_SELECTION_NORMAL:
            screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, false, EXTEND_CELL);
            break;
        case MOUSE_SELECTION_RECTANGLE:
            screen_start_selection(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, w->mouse_pos.in_left_half_of_cell, true, EXTEND_CELL);
            break;
        case MOUSE_SELECTION_WORD:
            if (screen_selection_range_for_word(screen, w->mouse_pos.cell_x, w->mouse_pos.cell_y, &y1, &y2, &start, &end, true)) S(EXTEND_WORD);
            break;
        case MOUSE_SELECTION_LINE:
            if (screen_selection_range_for_line(screen, w->mouse_pos.cell_y, &start, &end)) S(EXTEND_LINE);
            break;
        case MOUSE_SELECTION_LINE_FROM_BEGIN:
            if (screen_selection_range_for_line(screen, w->mouse_pos.cell_y, &start, &end)) S(EXTEND_LINE_FROM_BEGIN);
            break;
        case MOUSE_SELECTION_LINE_FROM_POINT:
            if (screen_selection_range_for_line(screen, w->mouse_pos.cell_y, &start, &end) && end > w->mouse_pos.cell_x) S(EXTEND_LINE_FROM_POINT);
            break;
        case MOUSE_SELECTION_WORD_AND_LINE_FROM_POINT:
            if (screen_selection_range_for_line(screen, w->mouse_pos.cell_y, &start, &end) && end > w->mouse_pos.cell_x) S(EXTEND_WORD_AND_LINE_FROM_POINT);
            break;
        case MOUSE_SELECTION_EXTEND:
            extend_selection(w, false, true);
            break;
        case MOUSE_SELECTION_MOVE_END:
            extend_selection(w, false, false);
            break;
        case MOUSE_SELECTION_UPTO_SURROUNDING_WHITESPACE:
            // TODO: Implement me for people migrating from urxvt
            break;
    }
    set_mouse_cursor_when_dragging(screen);
#undef S
}

static const char*
border_name(int edges) {
    switch(edges) {
        case 0: return "none";
        case LEFT_EDGE: return "left";
        case RIGHT_EDGE: return "right";
        case TOP_EDGE: return "top";
        case BOTTOM_EDGE: return "bottom";
        case LEFT_EDGE | TOP_EDGE: return "top-left";
        case LEFT_EDGE | BOTTOM_EDGE: return "bottom-left";
        case RIGHT_EDGE | TOP_EDGE: return "top-right";
        case RIGHT_EDGE | BOTTOM_EDGE: return "bottom-right";
    }
    return "unknown";
}

void
mouse_event(const int button, int modifiers, int action) {
    MouseShape old_cursor = mouse_cursor_shape;
    unsigned int window_idx = 0;
    Window *w = NULL; OSWindow *osw = global_state.callback_os_window;

    if (OPT(debug_keyboard)) {
        if (button < 0) { debug("%s x: %.1f y: %.1f ", "\x1b[36mMove\x1b[m", osw->mouse_x, osw->mouse_y); }
        else { debug("%s mouse_button: %d %s", action == GLFW_RELEASE ? "\x1b[32mRelease\x1b[m" : "\x1b[31mPress\x1b[m", button, format_mods(modifiers)); }
    }
    if (global_state.redirect_mouse_handling) {
        MouseRegion r= mouse_region(false, false); w = r.window;
        call_boss(mouse_event, "OK iiii dd",
                (r.in_tab_bar ? Py_True : Py_False), (w ? w->id : 0),
                action, modifiers, button, currently_pressed_button(),
                osw->mouse_x, osw->mouse_y
        );
        debug("mouse handling redirected\n");
        return;
    }
    if (global_state.active_drag_in_window) {
        if (button == -1) {  // drag move
            w = window_for_id(global_state.active_drag_in_window);
            if (w) {
                if (currently_pressed_button() == global_state.active_drag_button) {
                    clamp_to_window = true;
                    Tab *t = osw->tabs + osw->active_tab;
                    for (window_idx = 0; window_idx < t->num_windows && t->windows[window_idx].id != w->id; window_idx++);
                    handle_move_event(w, currently_pressed_button(), modifiers, window_idx);
                    clamp_to_window = false;
                    debug("handled as drag move\n");
                    return;
                }
            }
        }
        else if (action == GLFW_RELEASE && button == global_state.active_drag_button) {
            w = window_for_id(global_state.active_drag_in_window);
            if (w) {
                end_drag(w);
                // Clear any stale suppress flag that was set during a focus-transfer
                // press, since the drag release bypasses handle_button_event where
                // it would normally be cleared.
                if (osw) osw->suppress_left_mouse_release = false;
                debug("handled as drag end\n");
                dispatch_possible_click(w, button, modifiers);
                return;
            }
        }
    }
    if (global_state.tracked_drag_in_window) {
        if (button == -1) {  // drag move
            w = window_for_id(global_state.tracked_drag_in_window);
            if (w) {
                if (currently_pressed_button() == GLFW_MOUSE_BUTTON_LEFT) {
                    if (w->render_data.screen->modes.mouse_tracking_mode >= MOTION_MODE && w->render_data.screen->modes.mouse_tracking_protocol == SGR_PIXEL_PROTOCOL) {
                        clamp_to_window = true;
                        Tab *t = osw->tabs + osw->active_tab;
                        for (window_idx = 0; window_idx < t->num_windows && t->windows[window_idx].id != w->id; window_idx++);
                        handle_move_event(w, global_state.tracked_drag_button, modifiers, window_idx);
                        clamp_to_window = false;
                        debug("sent to child as drag move\n");
                        return;
                    }
                }
            }
        } else if (action == GLFW_RELEASE && button == GLFW_MOUSE_BUTTON_LEFT) {
            w = window_for_id(global_state.tracked_drag_in_window);
            if (w) {
                zero_at_ptr(&w->drag_source.initial_left_press);
                if (w->render_data.screen->modes.mouse_tracking_mode >= BUTTON_MODE && w->render_data.screen->modes.mouse_tracking_protocol >= SGR_PROTOCOL) {
                    global_state.tracked_drag_in_window = 0;
                    clamp_to_window = true;
                    Tab *t = osw->tabs + osw->active_tab;
                    for (window_idx = 0; window_idx < t->num_windows && t->windows[window_idx].id != w->id; window_idx++);
                    debug("sent to child as drag end\n");
                    handle_button_event(w, button, modifiers, window_idx);
                    clamp_to_window = false;
                    return;
                }
            }
        }
    }
    if (global_state.active_drag_resize) {
        if (button < 0) {
            call_boss(drag_resize_update, "dd", osw->mouse_x, osw->mouse_y);
            debug("drag resize updated\n");
        } else if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_RELEASE) {
            call_boss(drag_resize_end, "");
            global_state.active_drag_resize = 0;
            mouse_cursor_shape = DEFAULT_POINTER;
            set_mouse_cursor(mouse_cursor_shape);
            debug("drag resize ended\n");
        }
        return;
    }
    MouseRegion r = mouse_region(true, true);
    w = r.window; window_idx = r.window_idx;
    set_currently_hovered_window(w && !r.window_border && !r.in_title_bar ? w->id : 0, modifiers, true);

    if (r.in_tab_bar || global_state.tab_being_dragged.id) {
        mouse_cursor_shape = POINTER_POINTER;
        handle_tab_bar_mouse(button, modifiers, action);
        debug("handled by tab bar\n");
    } else if ((r.in_title_bar && r.window) || global_state.window_being_dragged.id) {
        mouse_cursor_shape = POINTER_POINTER;
        Window *tw = r.window;
        if (!tw && global_state.window_being_dragged.id) {
            tw = window_for_window_id(global_state.window_being_dragged.id);
        }
        if (tw) handle_window_title_bar_mouse(tw, button, modifiers, action);
        debug("handled by window title bar\n");
    } else if (r.window_border) {
        debug("window border: %s window id: %llu\n", border_name(r.window_border), w ? w->id : 0);
        if (r.window_border & LEFT_EDGE) {
            if (r.window_border & TOP_EDGE) mouse_cursor_shape = NWSE_RESIZE_POINTER;
            else if (r.window_border & BOTTOM_EDGE) mouse_cursor_shape = NESW_RESIZE_POINTER;
            else mouse_cursor_shape = EW_RESIZE_POINTER;
        } else if (r.window_border & RIGHT_EDGE) {
            if (r.window_border & TOP_EDGE) mouse_cursor_shape = NESW_RESIZE_POINTER;
            else if (r.window_border & BOTTOM_EDGE) mouse_cursor_shape = NWSE_RESIZE_POINTER;
            else mouse_cursor_shape = EW_RESIZE_POINTER;
        } else if (r.window_border & (TOP_EDGE | BOTTOM_EDGE)) mouse_cursor_shape = NS_RESIZE_POINTER;
        if (w && button == GLFW_MOUSE_BUTTON_LEFT && w->render_data.screen) {
            RAII_PyObject(retval, PyObject_CallMethod(
                global_state.boss, "drag_resize_start", "iddKII", r.window_border,
                osw->mouse_x, osw->mouse_y, w->id,
                w->render_data.screen->cell_size.width, w->render_data.screen->cell_size.height));
            if (retval == NULL) { PyErr_Print(); return; }
            if (PyObject_IsTrue(retval)) global_state.active_drag_resize = w->id;
        }
    } else if (w) {
        debug("grabbed: %d\n", w->render_data.screen->modes.mouse_tracking_mode != 0);
        handle_event(w, button, modifiers, window_idx);
    } else if (button == GLFW_MOUSE_BUTTON_LEFT && osw->mouse_button_pressed[button]) {
        // initial click, clamp it to the closest window
        w = closest_window_for_event(&window_idx);
        if (w) {
            clamp_to_window = true;
            debug("grabbed: %d\n", w->render_data.screen->modes.mouse_tracking_mode != 0);
            handle_event(w, button, modifiers, window_idx);
            clamp_to_window = false;
        } else debug("no window for event\n");
    } else {
        mouse_cursor_shape = DEFAULT_POINTER;
        debug("\n");
    }
    if (mouse_cursor_shape != old_cursor) set_mouse_cursor(mouse_cursor_shape);
}

static int
scale_scroll(MouseTrackingMode mouse_tracking_mode, double offset, GLFWOffsetType offset_type, double *pending_scroll_pixels, int cell_size) {
// scale the scroll by the multiplier unless the mouse is grabbed. If the mouse is grabbed only change direction.
#define SCALE_SCROLL(which) { double scale = OPT(which); if (mouse_tracking_mode) scale /= fabs(scale); offset *= scale; }
    int s = 0;
    switch (offset_type) {
        case GLFW_SCROLL_OFFEST_HIGHRES: {
            SCALE_SCROLL(touch_scroll_multiplier);
            double pixels = *pending_scroll_pixels + offset;
            if (fabs(pixels) < cell_size) {
                *pending_scroll_pixels = pixels;
                return 0;
            }
            s = (int)round(pixels) / cell_size;
            *pending_scroll_pixels = pixels - s * cell_size;
        } break;
        case GLFW_SCROLL_OFFEST_V120: {
            SCALE_SCROLL(wheel_scroll_multiplier);
            const double offset_lines = offset / 120.;
            const double pixels = *pending_scroll_pixels + offset_lines * cell_size;
            if (fabs(pixels) < cell_size) {
                *pending_scroll_pixels = pixels;
                return 0;
            }
            s = (int)round(pixels) / cell_size;
            *pending_scroll_pixels = pixels - s * cell_size;
        } break;
        case GLFW_SCROLL_OFFSET_LINES: {
            SCALE_SCROLL(wheel_scroll_multiplier);
            s = (int) round(offset);
            if (offset != 0) {
                const int min_lines = mouse_tracking_mode ? 1 : OPT(wheel_scroll_min_lines);
                if (min_lines > 0 && abs(s) < min_lines) s = offset > 0 ? min_lines : -min_lines;
                // Always add the minimum number of lines when it is negative
                else if (min_lines < 0) s = offset > 0 ? s - min_lines : s + min_lines;
                // apparently on cocoa some mice generate really small yoffset values
                // when scrolling slowly https://github.com/kovidgoyal/kitty/issues/1238
                if (s == 0) s = offset > 0 ? 1 : -1;
            }
            *pending_scroll_pixels = 0;
        } break;
    }
    return s;
#undef SCALE_SCROLL
}

static const char*
scroll_offset_type(GLFWOffsetType t) {
    switch(t) {
        case GLFW_SCROLL_OFFSET_LINES: return "lines";
        case GLFW_SCROLL_OFFEST_V120: return "v120";
        case GLFW_SCROLL_OFFEST_HIGHRES: return "highres";
    }
    return "";
}

static const char*
scroll_phase(GLFWMomentumType t) {
    switch(t) {
        case GLFW_NO_MOMENTUM_DATA: return "none";
        case GLFW_MOMENTUM_PHASE_MAY_BEGIN: return "may_begin";
        case GLFW_MOMENTUM_PHASE_BEGAN: return "began";
        case GLFW_MOMENTUM_PHASE_ACTIVE: return "active";
        case GLFW_MOMENTUM_PHASE_STATIONARY: return "stationary";
        case GLFW_MOMENTUM_PHASE_CANCELED: return "cancelled";
        case GLFW_MOMENTUM_PHASE_ENDED: return "ended";
    }
    return "";
}


static inline bool
pixel_scroll_enabled_for_screen(const Screen *screen) {
    return OPT(pixel_scroll) && screen->linebuf == screen->main_linebuf;
}

void
scroll_event(const GLFWScrollEvent *ev) {
    debug("\x1b[36mScroll\x1b[m type=%s x: %f y: %f momentum: %s modifiers: %s\n", scroll_offset_type(ev->offset_type), ev->x_offset, ev->y_offset, scroll_phase(ev->momentum_type), format_mods(ev->keyboard_modifiers));
    static id_type window_for_momentum_scroll = 0;
    static bool main_screen_for_momentum_scroll = false;
    // allow scroll events even if window is not currently focused (in
    // which case on some platforms such as macOS the mouse location is zeroed so
    // window_for_event() does not work).
    OSWindow *osw = global_state.callback_os_window;
    if (!osw->is_focused && osw->handle) {
        double mouse_x, mouse_y;
        glfwGetCursorPos((GLFWwindow*)osw->handle, &mouse_x, &mouse_y);
        osw->mouse_x = mouse_x * osw->viewport_x_ratio;
        osw->mouse_y = mouse_y * osw->viewport_y_ratio;
    }
    MouseRegion r = mouse_region(false, true);
    Window *w = r.window;
    if (!w && !r.in_tab_bar) {
        // fallback to last active window
        Tab *t = osw->tabs + osw->active_tab;
        if (t) w = t->windows + t->active_window;
    }
    if (!w) return;
    // Also update mouse cursor position while kitty OS window is not focused.
    // Allows scroll events to be delivered to the child with correct pointer coordinates even when
    // the window is not focused on macOS
    if (!osw->is_focused) {
        unsigned int x = 0, y = 0;
        bool in_left_half_of_cell;
        if (cell_for_pos(w, &x, &y, &in_left_half_of_cell, osw)) {
            w->mouse_pos.cell_x = x; w->mouse_pos.cell_y = y;
            w->mouse_pos.in_left_half_of_cell = in_left_half_of_cell;
        }
    }
    Screen *screen = w->render_data.screen;

    switch(ev->momentum_type) {
        case GLFW_NO_MOMENTUM_DATA:
            break;
        case GLFW_MOMENTUM_PHASE_BEGAN:
            window_for_momentum_scroll = w->id;
            main_screen_for_momentum_scroll = screen->linebuf == screen->main_linebuf;
            break;
        case GLFW_MOMENTUM_PHASE_STATIONARY: case GLFW_MOMENTUM_PHASE_ACTIVE:
            if (window_for_momentum_scroll != w->id || main_screen_for_momentum_scroll != (screen->linebuf == screen->main_linebuf)) return;
            break;
        case GLFW_MOMENTUM_PHASE_ENDED: case GLFW_MOMENTUM_PHASE_CANCELED:
            window_for_momentum_scroll = 0;
            break;
        case GLFW_MOMENTUM_PHASE_MAY_BEGIN:
            break;
    }
    finish_scroll_animation(screen);
    if (ev->y_offset != 0.0) {
        if (screen->modes.mouse_tracking_mode == NO_TRACKING && pixel_scroll_enabled_for_screen(screen) && (ev->offset_type == GLFW_SCROLL_OFFEST_HIGHRES || ev->offset_type == GLFW_SCROLL_OFFEST_V120)) {
            double delta_pixels;
            if (ev->offset_type == GLFW_SCROLL_OFFEST_HIGHRES) {
                delta_pixels = ev->y_offset * OPT(touch_scroll_multiplier);
            } else {
                const double offset_lines = (ev->y_offset / 120.) * OPT(wheel_scroll_multiplier);
                delta_pixels = offset_lines * global_state.callback_os_window->fonts_data->fcm.cell_height;
            }
            screen->pending_scroll_pixels_y = 0.0;
            if (screen_apply_pixel_scroll(screen, delta_pixels) && screen->selections.in_progress) update_drag(w);
        } else {
            int s = scale_scroll(screen->modes.mouse_tracking_mode, ev->y_offset, ev->offset_type, &screen->pending_scroll_pixels_y, global_state.callback_os_window->fonts_data->fcm.cell_height);
            if (s) {
                bool upwards = s > 0;
                if (screen->modes.mouse_tracking_mode) {
                    int sz = encode_mouse_scroll(w, upwards ? 4 : 5, ev->keyboard_modifiers);
                    if (sz > 0) {
                        mouse_event_buf[sz] = 0;
                        for (s = abs(s); s > 0; s--) {
                            write_escape_code_to_child(screen, ESC_CSI, mouse_event_buf);
                        }
                    }
                } else {
                        if (screen->linebuf == screen->main_linebuf) {
                            screen_history_scroll(screen, abs(s), upwards);
                            if (screen->selections.in_progress) update_drag(w);
                        }
                        else fake_scroll(w, abs(s), upwards);
                    }
                }
        }
    }
    if (ev->x_offset != 0.0) {
        int s = scale_scroll(screen->modes.mouse_tracking_mode, ev->x_offset, ev->offset_type, &screen->pending_scroll_pixels_x, global_state.callback_os_window->fonts_data->fcm.cell_width);
        if (s) {
            if (screen->modes.mouse_tracking_mode) {
                int sz = encode_mouse_scroll(w, s > 0 ? 6 : 7, ev->keyboard_modifiers);
                if (sz > 0) {
                    mouse_event_buf[sz] = 0;
                    for (s = abs(s); s > 0; s--) {
                        write_escape_code_to_child(screen, ESC_CSI, mouse_event_buf);
                    }
                }
            }
        }
    }
}

static PyObject*
send_mouse_event(PyObject *self UNUSED, PyObject *args, PyObject *kw) {
    Screen *screen;
    int x, y, px=0, py=0, in_left_half_of_cell=0;

    int button, action, mods;
    static const char* kwlist[] = {"screen", "cell_x", "cell_y", "button", "action", "mods", "pixel_x", "pixel_y", "in_left_half_of_cell", NULL};
    if (!PyArg_ParseTupleAndKeywords(args, kw, "O!iiiii|iip", (char**)kwlist,
                &Screen_Type, &screen, &x, &y, &button, &action, &mods, &px, &py, &in_left_half_of_cell)) return NULL;

    MouseTrackingMode mode = screen->modes.mouse_tracking_mode;
    if (mode == ANY_MODE || (mode == MOTION_MODE && action != MOVE) || (mode == BUTTON_MODE && (action == PRESS || action == RELEASE))) {
        MousePosition mpos = {.cell_x = x, .cell_y = y, .global_x = px, .global_y = py, .in_left_half_of_cell = in_left_half_of_cell};
        int sz = encode_mouse_event_impl(&mpos, screen->modes.mouse_tracking_protocol, button, action, mods);
        if (sz > 0) {
            mouse_event_buf[sz] = 0;
            write_escape_code_to_child(screen, ESC_CSI, mouse_event_buf);
            Py_RETURN_TRUE;
        }
    }
    Py_RETURN_FALSE;
}

static PyObject*
test_encode_mouse(PyObject *self UNUSED, PyObject *args) {
    unsigned int x, y;
    int mouse_tracking_protocol, button, action, mods;
    if (!PyArg_ParseTuple(args, "IIiiii", &x, &y, &mouse_tracking_protocol, &button, &action, &mods)) return NULL;
    MousePosition mpos = {.cell_x = x - 1, .cell_y = y - 1};
    int sz = encode_mouse_event_impl(&mpos, mouse_tracking_protocol, button, action, mods);
    return PyUnicode_FromStringAndSize(mouse_event_buf, sz);
}

static PyObject*
mock_mouse_selection(PyObject *self UNUSED, PyObject *args) {
    PyObject *capsule;
    int button, code;
    if (!PyArg_ParseTuple(args, "O!ii", &PyCapsule_Type, &capsule, &button, &code)) return NULL;
    Window *w = PyCapsule_GetPointer(capsule, "Window");
    if (!w) return NULL;
    mouse_selection(w, code, button);
    Py_RETURN_NONE;
}

static PyObject*
send_mock_mouse_event_to_window(PyObject *self UNUSED, PyObject *args) {
    PyObject *capsule;
    int button, modifiers, is_release, clear_clicks, in_left_half_of_cell;
    unsigned int x, y;
    if (!PyArg_ParseTuple(args, "O!iipIIpp", &PyCapsule_Type, &capsule, &button, &modifiers, &is_release, &x, &y, &clear_clicks, &in_left_half_of_cell)) return NULL;
    Window *w = PyCapsule_GetPointer(capsule, "Window");
    if (!w) return NULL;
    if (clear_clicks) clear_click_queue(w, button);
    bool mouse_cell_changed = x != w->mouse_pos.cell_x || y != w->mouse_pos.cell_y || w->mouse_pos.in_left_half_of_cell != in_left_half_of_cell;
    w->mouse_pos.global_x = 10 * x; w->mouse_pos.global_y = 20 * y;
    w->mouse_pos.cell_x = x; w->mouse_pos.cell_y = y;
    w->mouse_pos.in_left_half_of_cell = in_left_half_of_cell;
    static int last_button_pressed = GLFW_MOUSE_BUTTON_LEFT;
    if (button < 0) {
        if (button == -2) do_drag_scroll(w, true);
        else if (button == -3) do_drag_scroll(w, false);
        else handle_mouse_movement_in_kitty(w, last_button_pressed, mouse_cell_changed);
    } else {
        if (global_state.active_drag_in_window && is_release && button == global_state.active_drag_button) {
            end_drag(w);
        } else {
            dispatch_mouse_event(w, button, is_release ? -1 : 1, modifiers, false);
            if (!is_release) {
                last_button_pressed = button;
                add_press(w, button, modifiers);
            }
        }
    }
    Py_RETURN_NONE;
}

static PyMethodDef module_methods[] = {
    {"send_mouse_event", (PyCFunction)(void (*) (void))(send_mouse_event), METH_VARARGS | METH_KEYWORDS, NULL},
    METHODB(test_encode_mouse, METH_VARARGS),
    METHODB(send_mock_mouse_event_to_window, METH_VARARGS),
    METHODB(mock_mouse_selection, METH_VARARGS),
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

bool
init_mouse(PyObject *module) {
    PyModule_AddIntMacro(module, PRESS);
    PyModule_AddIntMacro(module, RELEASE);
    PyModule_AddIntMacro(module, DRAG);
    PyModule_AddIntMacro(module, MOVE);
    PyModule_AddIntMacro(module, MOUSE_SELECTION_NORMAL);
    PyModule_AddIntMacro(module, MOUSE_SELECTION_EXTEND);
    PyModule_AddIntMacro(module, MOUSE_SELECTION_RECTANGLE);
    PyModule_AddIntMacro(module, MOUSE_SELECTION_WORD);
    PyModule_AddIntMacro(module, MOUSE_SELECTION_LINE);
    PyModule_AddIntMacro(module, MOUSE_SELECTION_LINE_FROM_BEGIN);
    PyModule_AddIntMacro(module, MOUSE_SELECTION_LINE_FROM_POINT);
    PyModule_AddIntMacro(module, MOUSE_SELECTION_WORD_AND_LINE_FROM_POINT);
    PyModule_AddIntMacro(module, MOUSE_SELECTION_MOVE_END);
    PyModule_AddIntMacro(module, MOUSE_SELECTION_UPTO_SURROUNDING_WHITESPACE);
    if (PyModule_AddFunctions(module, module_methods) != 0) return false;
    return true;
}
