diff --git a/input.ts b/input.ts index fa54e1c..6004c30 100644 --- a/input.ts +++ b/input.ts @@ -163,10 +163,31 @@ export interface KeyUp extends KeyInfo { export type KeyEvent = KeyDown | KeyRepeat | KeyUp; /** - * A mouse button was pressed or released + * A mouse button was pressed. */ -export interface MouseEvent extends KeyModifiers { - type: "mouse"; +export interface MouseDownEvent extends KeyModifiers { + type: "mousedown"; + /** + * Which mouse button triggered this event. + */ + button: "left" | "right" | "middle"; + + /** + * `x` coordinate of this event + */ + x: number; + + /** + * `y` coordinate of this event + */ + y: number; +} + +/** + * A mouse button was released. + */ +export interface MouseUpEvent extends KeyModifiers { + type: "mouseup"; /** * Which mouse button triggered this event. * @@ -185,18 +206,13 @@ export interface MouseEvent extends KeyModifiers { * `y` coordinate of this event */ y: number; - - /** - * True if the event represents a button being released. - */ - release?: true; } /** * Mouse movement while a button is held. */ -export interface DragEvent extends KeyModifiers { - type: "drag"; +export interface MouseMoveEvent extends KeyModifiers { + type: "mousemove"; /** * Which mouse button is being held during the drag. */ @@ -251,8 +267,9 @@ export interface ResizeEvent { export type InputEvent = | KeyEvent - | MouseEvent - | DragEvent + | MouseDownEvent + | MouseUpEvent + | MouseMoveEvent | WheelEvent | ResizeEvent; @@ -441,7 +458,7 @@ const KEY_NAMES = new Map([ [KEY_SCROLL_LOCK, "ScrollLock"], ]); -const BUTTON_NAMES = new Map([ +const BUTTON_NAMES = new Map([ [KEY_MOUSE_LEFT, "left"], [KEY_MOUSE_RIGHT, "right"], [KEY_MOUSE_MIDDLE, "middle"], @@ -526,7 +543,7 @@ function mapEvent(native: NativeInputEvent): InputEvent { if (native.mod & MOD_MOTION) { let button = BUTTON_NAMES.get(native.key) ?? "left"; return { - type: "drag" as const, + type: "mousemove" as const, button: button === "release" ? "left" as const : button, x: native.x, y: native.y, @@ -534,16 +551,23 @@ function mapEvent(native: NativeInputEvent): InputEvent { }; } let button = BUTTON_NAMES.get(native.key) ?? "left"; - let m = mods(native); - let ev: MouseEvent = { - type: "mouse", - button, - x: native.x, - y: native.y, - ...m, - }; - if (native.mod & MOD_RELEASE) ev.release = true; - return ev; + if (native.mod & MOD_RELEASE) { + return { + type: "mouseup" as const, + button, + x: native.x, + y: native.y, + ...mods(native), + }; + } else { + return { + type: "mousedown" as const, + button: button as MouseDownEvent["button"], + x: native.x, + y: native.y, + ...mods(native), + }; + } } case EVENT_RESIZE: { return { type: "resize", width: native.w, height: native.h }; diff --git a/src/input.c b/src/input.c index 1493c05..af96707 100644 --- a/src/input.c +++ b/src/input.c @@ -61,6 +61,15 @@ static struct InputEvent *emit(struct InputState *st) { #define MOUSE_BUTTON_RELEASE 3 #define MOUSE_WHEEL_BIT 64 +static uint8_t mouse_mods(int b) { + uint8_t m = 0; + if (b & 4) m |= MOD_SHIFT; + if (b & 8) m |= MOD_ALT; + if (b & 16) m |= MOD_CTRL; + if (b & 32) m |= MOD_MOTION; + return m; +} + static uint16_t mouse_button(int b) { switch (b & MOUSE_BUTTON_MASK) { case MOUSE_BUTTON_LEFT: @@ -94,7 +103,7 @@ static int parse_mouse_vt200(struct InputState *st, struct InputEvent *ev) { int b = st->buf[3] - 0x20; ev->type = EVENT_MOUSE; ev->key = mouse_button(b); - ev->mod = (b & 32) ? MOD_MOTION : 0; + ev->mod = mouse_mods(b); if (ev->key == KEY_MOUSE_RELEASE) ev->mod |= MOD_RELEASE; ev->x = ((uint8_t)st->buf[4]) - 0x21; @@ -139,7 +148,7 @@ static int parse_mouse_sgr(struct InputState *st, struct InputEvent *ev) { ev->type = EVENT_MOUSE; ev->key = mouse_button(num[0]); - ev->mod = (num[0] & 32) ? MOD_MOTION : 0; + ev->mod = mouse_mods(num[0]); if (trail == 'm') ev->mod |= MOD_RELEASE; ev->x = (num[1] - 1 < 0) ? 0 : num[1] - 1; @@ -185,7 +194,7 @@ static int parse_mouse_urxvt(struct InputState *st, struct InputEvent *ev) { int b = num[0] - 0x20; ev->type = EVENT_MOUSE; ev->key = mouse_button(b); - ev->mod = (b & 32) ? MOD_MOTION : 0; + ev->mod = mouse_mods(b); if (ev->key == KEY_MOUSE_RELEASE) ev->mod |= MOD_RELEASE; ev->x = (num[1] - 1 < 0) ? 0 : num[1] - 1; diff --git a/test/input.test.ts b/test/input.test.ts index 3b7deff..8721069 100644 --- a/test/input.test.ts +++ b/test/input.test.ts @@ -160,7 +160,7 @@ describe("input", () => { let result = input.scan(str("\x1b[<0;35;12M")); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "mouse", + type: "mousedown", button: "left", x: 34, y: 11, @@ -171,11 +171,10 @@ describe("input", () => { let result = input.scan(str("\x1b[<0;10;5m")); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "mouse", + type: "mouseup", button: "left", x: 9, y: 4, - release: true, }); }); @@ -183,11 +182,10 @@ describe("input", () => { let result = input.scan(str("\x1b[<2;10;5m")); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "mouse", + type: "mouseup", button: "right", x: 9, y: 4, - release: true, }); }); @@ -195,11 +193,10 @@ describe("input", () => { let result = input.scan(str("\x1b[<1;10;5m")); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "mouse", + type: "mouseup", button: "middle", x: 9, y: 4, - release: true, }); }); @@ -208,11 +205,10 @@ describe("input", () => { let result = input.scan(bytes(0x1b, 0x5b, 0x4d, 0x23, 0x2b, 0x26)); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "mouse", + type: "mouseup", button: "release", x: 10, y: 5, - release: true, }); }); @@ -243,12 +239,65 @@ describe("input", () => { let result = input.scan(bytes(0x1b, 0x5b, 0x4d, 0x20, 0x2b, 0x26)); expect(result.events.length).toBe(1); expect(result.events[0]).toMatchObject({ - type: "mouse", + type: "mousedown", button: "left", x: 10, y: 5, }); }); + + it("parses SGR mouse press with shift", () => { + // btn=4 (shift bit) => left + shift + let result = input.scan(str("\x1b[<4;10;5M")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "mousedown", + button: "left", + x: 9, + y: 4, + shift: true, + }); + }); + + it("parses SGR mouse press with ctrl", () => { + // btn=16 (ctrl bit) => left + ctrl + let result = input.scan(str("\x1b[<16;10;5M")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "mousedown", + button: "left", + x: 9, + y: 4, + ctrl: true, + }); + }); + + it("parses SGR mouse press with alt", () => { + // btn=8 (alt bit) => left + alt + let result = input.scan(str("\x1b[<8;10;5M")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "mousedown", + button: "left", + x: 9, + y: 4, + alt: true, + }); + }); + + it("parses SGR mouse press with ctrl+shift", () => { + // btn=20 (ctrl=16 + shift=4) => left + ctrl + shift + let result = input.scan(str("\x1b[<20;10;5M")); + expect(result.events.length).toBe(1); + expect(result.events[0]).toMatchObject({ + type: "mousedown", + button: "left", + x: 9, + y: 4, + ctrl: true, + shift: true, + }); + }); }); describe("ESC timeout", () => {