diff --git a/builddefs/common_features.mk b/builddefs/common_features.mk
index bb272099a67e..757acdead662 100644
--- a/builddefs/common_features.mk
+++ b/builddefs/common_features.mk
@@ -121,6 +121,11 @@ ifeq ($(strip $(MOUSEKEY_ENABLE)), yes)
MOUSE_ENABLE := yes
endif
+ifeq ($(strip $(MOUSEGRID_ENABLE)), yes)
+ DIGITIZER_ENABLE := yes
+ DEFERRED_EXEC_ENABLE := yes
+endif
+
VALID_POINTING_DEVICE_DRIVER_TYPES := adns5050 adns9800 analog_joystick azoteq_iqs5xx cirque_pinnacle_i2c cirque_pinnacle_spi paw3204 pmw3320 pmw3360 pmw3389 pimoroni_trackball custom
ifeq ($(strip $(POINTING_DEVICE_ENABLE)), yes)
ifeq ($(filter $(POINTING_DEVICE_DRIVER),$(VALID_POINTING_DEVICE_DRIVER_TYPES)),)
diff --git a/builddefs/generic_features.mk b/builddefs/generic_features.mk
index f14f44087702..d29a496a1ce0 100644
--- a/builddefs/generic_features.mk
+++ b/builddefs/generic_features.mk
@@ -40,6 +40,7 @@ GENERIC_FEATURES = \
LEADER \
MAGIC \
MOUSEKEY \
+ MOUSEGRID \
MUSIC \
OS_DETECTION \
PROGRAMMABLE_BUTTON \
diff --git a/builddefs/show_options.mk b/builddefs/show_options.mk
index 1c1a3ebf8e0a..5b2968970f10 100644
--- a/builddefs/show_options.mk
+++ b/builddefs/show_options.mk
@@ -1,6 +1,7 @@
BUILD_OPTION_NAMES = \
BOOTMAGIC_ENABLE \
MOUSEKEY_ENABLE \
+ MOUSEGRID_ENABLE \
EXTRAKEY_ENABLE \
CONSOLE_ENABLE \
COMMAND_ENABLE \
diff --git a/data/constants/keycodes/keycodes_0.0.6_basic.hjson b/data/constants/keycodes/keycodes_0.0.6_basic.hjson
new file mode 100644
index 000000000000..597d013b4a9c
--- /dev/null
+++ b/data/constants/keycodes/keycodes_0.0.6_basic.hjson
@@ -0,0 +1,116 @@
+{
+ "keycodes": {
+ "0x00E8": {
+ "group": "mousegrid",
+ "key": "QK_MOUSE_GRID_TOP_LEFT",
+ "label": "Select top left mouse grid",
+ "aliases": [
+ "MG_TL"
+ ]
+ },
+ "0x00E9": {
+ "group": "mousegrid",
+ "key": "QK_MOUSE_GRID_TOP",
+ "label": "Select top mouse grid",
+ "aliases": [
+ "MG_T"
+ ]
+ },
+ "0x00F0": {
+ "group": "mousegrid",
+ "key": "QK_MOUSE_GRID_TOP_RIGHT",
+ "label": "Select top right mouse grid",
+ "aliases": [
+ "MG_TR"
+ ]
+ },
+ "0x00F1": {
+ "group": "mousegrid",
+ "key": "QK_MOUSE_GRID_LEFT",
+ "label": "Select left mouse grid",
+ "aliases": [
+ "MG_L"
+ ]
+ },
+ "0x00F2": {
+ "group": "mousegrid",
+ "key": "QK_MOUSE_GRID_CENTER",
+ "label": "Select center mouse grid",
+ "aliases": [
+ "MG_C"
+ ]
+ },
+ "0x00F3": {
+ "group": "mousegrid",
+ "key": "QK_MOUSE_GRID_RIGHT",
+ "label": "Select right mouse grid",
+ "aliases": [
+ "MG_R"
+ ]
+ },
+ "0x00F4": {
+ "group": "mousegrid",
+ "key": "QK_MOUSE_GRID_BOTTOM_LEFT",
+ "label": "Select bottom left mouse grid",
+ "aliases": [
+ "MG_BL"
+ ]
+ },
+ "0x00F5": {
+ "group": "mousegrid",
+ "key": "QK_MOUSE_GRID_BOTTOM",
+ "label": "Select bottom mouse grid",
+ "aliases": [
+ "MG_B"
+ ]
+ },
+ "0x00F6": {
+ "group": "mousegrid",
+ "key": "QK_MOUSE_GRID_BOTTOM_RIGHT",
+ "label": "Select bottom right mouse grid",
+ "aliases": [
+ "MG_BR"
+ ]
+ },
+ "0x00F7": {
+ "group": "mousegrid",
+ "key": "QK_MOUSE_GRID_RESET",
+ "label": "Reset mouse grid cursor",
+ "aliases": [
+ "MG_RST"
+ ]
+ },
+ "0x00F8": {
+ "group": "mousegrid",
+ "key": "QK_MOUSE_GRID_ANIMATE",
+ "label": "Visualize mouse grid options",
+ "aliases": [
+ "MG_ANIM"
+ ]
+ },
+ "0x00F9": {
+ "group": "mousegrid",
+ "key": "QK_MOUSE_GRID_UNDO",
+ "label": "Undo a mousegrid move",
+ "aliases": [
+ "MG_UNDO"
+ ]
+ },
+ "0x00FA": {
+ "group": "mousegrid",
+ "key": "QK_MOUSE_GRID_ALPHABET",
+ "label": "Initiate an alphabet mousegrid move",
+ "aliases": [
+ "MG_ABC"
+ ]
+ },
+ "0x00FB": {
+ "group": "mousegrid",
+ "key": "QK_MOUSE_GRID_NEAR",
+ "label": "Reset cursor scale for local movement",
+ "aliases": [
+ "MG_NEAR"
+ ]
+ },
+ }
+}
diff --git a/docs/features/mouse_grid.md b/docs/features/mouse_grid.md
new file mode 100644
index 000000000000..08c94b6d2830
--- /dev/null
+++ b/docs/features/mouse_grid.md
@@ -0,0 +1,85 @@
+# Mouse grid
+
+Mouse grid is a feature for quickly positioning the mouse cursor using coordinate or grid based methods.
+
+### Enabling mouse grid
+
+To enable mouse grid, add the following line to your keymap’s `rules.mk`:
+
+```c
+MOUSEGRID_ENABLE = yes
+```
+
+## Alphabet-based coordinates
+
+One of the methods is to divide the screen into a 26x26 grid and position the mouse by inputting 2 characters for coordinates.
+
+This is triggered by `QK_MOUSE_GRID_ALPHABET` (or `MG_ABC`) key that will wait for a few seconds for 2 keypresses that will be interpreted as characters specifying the coordinates where to place the mouse.
+
+That is, when pressing `MG_ABC`, `KC_M`, `KC_P`, the mouse would be positioned:
+
+| |A|B|C|…|L|M|N|…|X|Y|Z|
+|-|-|-|-|-|-|-|-|-|-|-|-|
+|A| | | | | | | | | | | |
+|B| | | | | | | | | | | |
+|C| | | | | | | | | | | |
+|…| | | | | | | | | | | |
+|O| | | | | | | | | | | |
+|P| | | | | |X| | | | | |
+|R| | | | | | | | | | | |
+|…| | | | | | | | | | | |
+|X| | | | | | | | | | | |
+|Y| | | | | | | | | | | |
+|Z| | | | | | | | | | | |
+
+## Repeated directional grid
+
+The other method works by repeatedly picking from a 3x3 grid to repeatedly zoom in towards the desired cursor position.
+
+This is triggered by `QK_MOUSE_GRID_RESET` (or `MG_RST`) to center the cursor and then repeatedly picking one of the 9 positions in the grid:
+
+| |-1 |0 |+1 |
+|--|-------|------|-------|
+|-1|`MG_TL`|`MG_T`|`MG_TR`|
+|0 |`MG_L` |`MG_C`|`MG_R` |
+|+1|`MG_BL`|`MG_B`|`MG_BR`|
+
+In first interation, the whole screen is divided into a 3x3 grid to pick from, while second iteration picks within the 3x3 subgrid of the "quadrant" picked in the first iteration.
+
+Since the "quadrants" are not visualized on screen while navigating, if a movement target is kind of between two quadrants, it can be difficult to tell which one is the "correct" coordinate.
+
+In order to be forgiving of some inaccuracy, the default configuration leaves a little bit of buffer by zooming in slightly less than would be required for the 1/9th of the "quadrant". This means that for slightly inaccurate path can still reach the target but may require an extra keystroke.
+
+While practicing, `QK_MOUSE_GRID_ANIMATE` (or `MG_ANIM`) can be pressed to cause the cursor to quickly jump through all the 9 positions of each quadrant choice. Additionally, `QK_MOUSE_GRID_UNDO` (or `MG_UNDO`) can be used to undo the last quadrant choice.
+
+## Precision
+
+On a 1920x1080 screen an alphabet of 26 characters divides the screen into grid of ~75x50 rectangles. This can be enough for larger UI elements (eg: browser tabs) but to accurately point at dense UI elements (eg: menu items).
+
+On a 1920x1080 screen it takes 4 iterations of 3x3 grid to point at a grid of ~35x20 rectangles, which allows pointing at event relatively small UI elements, like menu items.
+
+Both grid methods can combined, but for fine adjustments it may be better to use the mouse keys feature.
+
+However, the `QK_MOUSE_GRID_NEAR` (or `MG_NEAR`) key can be used to to zoom out the cursor tot he local area, so that next keystrokes would allow move to a nearby UI element faster, without resetting to the center of the screen.
+
+## Mouse grid keycodes
+
+Full list of mouse grid keycodes:
+
+|Key |Alias |Description |
+|----------------------------|---------|------------------------------------------------|
+|`QK_MOUSE_GRID_ALPHABET` |`MG_ABC` |Position cursor according to alphabet-based grid|
+|`QK_MOUSE_GRID_RESET` |`MG_RST` |Reset cursor to the center of the screen |
+|`QK_MOUSE_GRID_TOP_LEFT` |`MG_TL` |Pick top-left quadrant |
+|`QK_MOUSE_GRID_TOP` |`MG_T` |Pick top quadrant |
+|`QK_MOUSE_GRID_TOP_RIGHT` |`MG_TR` |Pick top-right quadrant |
+|`QK_MOUSE_GRID_LEFT` |`MG_L` |Pick left quadrant |
+|`QK_MOUSE_GRID_CENTER` |`MG_C` |Pick center quadrant |
+|`QK_MOUSE_GRID_RIGHT` |`MG_R` |Pick right quadrant |
+|`QK_MOUSE_GRID_BOTTOM_LEFT` |`MG_BL` |Pick bottom-left quadrant |
+|`QK_MOUSE_GRID_BOTTOM` |`MG_B` |Pick bottom quadrant |
+|`QK_MOUSE_GRID_BOTTOM_RIGHT`|`MG_BR` |Pick bottom-right quadrant |
+|`QK_MOUSE_GRID_ANIMATE` |`MG_ANIM`|Quickly move the cursor through all quadrants |
+|`QK_MOUSE_GRID_UNDO` |`MG_UNDO`|Undo last quadrant pick |
+|`QK_MOUSE_GRID_NEAR` |`MG_NEAR`|Zoom cursor to local/nearby quadrant navigation |
+
diff --git a/quantum/keyboard.c b/quantum/keyboard.c
index d7836cf36e00..f77a9a5f1fd0 100644
--- a/quantum/keyboard.c
+++ b/quantum/keyboard.c
@@ -47,6 +47,9 @@ along with this program. If not, see .
#ifdef MOUSEKEY_ENABLE
# include "mousekey.h"
#endif
+#ifdef MOUSEGRID_ENABLE
+# include "mousegrid.h"
+#endif
#ifdef PS2_MOUSE_ENABLE
# include "ps2_mouse.h"
#endif
@@ -488,6 +491,9 @@ void keyboard_init(void) {
#ifdef HAPTIC_ENABLE
haptic_init();
#endif
+#ifdef MOUSEKEY_ENABLE
+ mousegrid_init();
+#endif
#if defined(DEBUG_MATRIX_SCAN_RATE) && defined(CONSOLE_ENABLE)
debug_enable = true;
diff --git a/quantum/keycodes.h b/quantum/keycodes.h
index 921dc7199f26..2be9889c9426 100644
--- a/quantum/keycodes.h
+++ b/quantum/keycodes.h
@@ -1,4 +1,4 @@
-// Copyright 2024 QMK
+// Copyright 2025 QMK
// SPDX-License-Identifier: GPL-2.0-or-later
/*******************************************************************************
@@ -314,6 +314,20 @@ enum qk_keycode_defines {
KC_RIGHT_SHIFT = 0x00E5,
KC_RIGHT_ALT = 0x00E6,
KC_RIGHT_GUI = 0x00E7,
+ QK_MOUSE_GRID_TOP_LEFT = 0x00E8,
+ QK_MOUSE_GRID_TOP = 0x00E9,
+ QK_MOUSE_GRID_TOP_RIGHT = 0x00F0,
+ QK_MOUSE_GRID_LEFT = 0x00F1,
+ QK_MOUSE_GRID_CENTER = 0x00F2,
+ QK_MOUSE_GRID_RIGHT = 0x00F3,
+ QK_MOUSE_GRID_BOTTOM_LEFT = 0x00F4,
+ QK_MOUSE_GRID_BOTTOM = 0x00F5,
+ QK_MOUSE_GRID_BOTTOM_RIGHT = 0x00F6,
+ QK_MOUSE_GRID_RESET = 0x00F7,
+ QK_MOUSE_GRID_ANIMATE = 0x00F8,
+ QK_MOUSE_GRID_UNDO = 0x00F9,
+ QK_MOUSE_GRID_ALPHABET = 0x00FA,
+ QK_MOUSE_GRID_NEAR = 0x00FB,
QK_SWAP_HANDS_TOGGLE = 0x56F0,
QK_SWAP_HANDS_TAP_TOGGLE = 0x56F1,
QK_SWAP_HANDS_MOMENTARY_ON = 0x56F2,
@@ -977,6 +991,20 @@ enum qk_keycode_defines {
KC_RGUI = KC_RIGHT_GUI,
KC_RCMD = KC_RIGHT_GUI,
KC_RWIN = KC_RIGHT_GUI,
+ MG_TL = QK_MOUSE_GRID_TOP_LEFT,
+ MG_T = QK_MOUSE_GRID_TOP,
+ MG_TR = QK_MOUSE_GRID_TOP_RIGHT,
+ MG_L = QK_MOUSE_GRID_LEFT,
+ MG_C = QK_MOUSE_GRID_CENTER,
+ MG_R = QK_MOUSE_GRID_RIGHT,
+ MG_BL = QK_MOUSE_GRID_BOTTOM_LEFT,
+ MG_B = QK_MOUSE_GRID_BOTTOM,
+ MG_BR = QK_MOUSE_GRID_BOTTOM_RIGHT,
+ MG_RST = QK_MOUSE_GRID_RESET,
+ MG_ANIM = QK_MOUSE_GRID_ANIMATE,
+ MG_UNDO = QK_MOUSE_GRID_UNDO,
+ MG_ABC = QK_MOUSE_GRID_ALPHABET,
+ MG_NEAR = QK_MOUSE_GRID_NEAR,
SH_TOGG = QK_SWAP_HANDS_TOGGLE,
SH_TT = QK_SWAP_HANDS_TAP_TOGGLE,
SH_MON = QK_SWAP_HANDS_MOMENTARY_ON,
@@ -1491,6 +1519,7 @@ enum qk_keycode_defines {
#define IS_CONSUMER_KEYCODE(code) ((code) >= KC_AUDIO_MUTE && (code) <= KC_LAUNCHPAD)
#define IS_MOUSE_KEYCODE(code) ((code) >= QK_MOUSE_CURSOR_UP && (code) <= QK_MOUSE_ACCELERATION_2)
#define IS_MODIFIER_KEYCODE(code) ((code) >= KC_LEFT_CTRL && (code) <= KC_RIGHT_GUI)
+#define IS_MOUSEGRID_KEYCODE(code) ((code) >= QK_MOUSE_GRID_TOP_LEFT && (code) <= QK_MOUSE_GRID_NEAR)
#define IS_SWAP_HANDS_KEYCODE(code) ((code) >= QK_SWAP_HANDS_TOGGLE && (code) <= QK_SWAP_HANDS_ONE_SHOT)
#define IS_MAGIC_KEYCODE(code) ((code) >= QK_MAGIC_SWAP_CONTROL_CAPS_LOCK && (code) <= QK_MAGIC_TOGGLE_ESCAPE_CAPS_LOCK)
#define IS_MIDI_KEYCODE(code) ((code) >= QK_MIDI_ON && (code) <= QK_MIDI_PITCH_BEND_UP)
@@ -1517,6 +1546,7 @@ enum qk_keycode_defines {
#define CONSUMER_KEYCODE_RANGE KC_AUDIO_MUTE ... KC_LAUNCHPAD
#define MOUSE_KEYCODE_RANGE QK_MOUSE_CURSOR_UP ... QK_MOUSE_ACCELERATION_2
#define MODIFIER_KEYCODE_RANGE KC_LEFT_CTRL ... KC_RIGHT_GUI
+#define MOUSEGRID_KEYCODE_RANGE QK_MOUSE_GRID_TOP_LEFT ... QK_MOUSE_GRID_NEAR
#define SWAP_HANDS_KEYCODE_RANGE QK_SWAP_HANDS_TOGGLE ... QK_SWAP_HANDS_ONE_SHOT
#define MAGIC_KEYCODE_RANGE QK_MAGIC_SWAP_CONTROL_CAPS_LOCK ... QK_MAGIC_TOGGLE_ESCAPE_CAPS_LOCK
#define MIDI_KEYCODE_RANGE QK_MIDI_ON ... QK_MIDI_PITCH_BEND_UP
diff --git a/quantum/mousegrid.c b/quantum/mousegrid.c
new file mode 100644
index 000000000000..0e93e31218ec
--- /dev/null
+++ b/quantum/mousegrid.c
@@ -0,0 +1,325 @@
+#include
+
+#include "action.h"
+#include "deferred_exec.h"
+#include "digitizer.h"
+#include "keycodes.h"
+#include "keymap_introspection.h"
+#include "mousegrid.h"
+#include "print.h"
+#include "progmem.h"
+#include "timer.h"
+
+#define MG_ANIMATION_SLEEP 5
+
+typedef struct {
+ // Cursor x/y position from 0...1
+ float x, y;
+ // Bounding box width/height for the next cursor movement
+ float scale_x, scale_y;
+} mousegrid_cursor_t;
+
+// Current active grid cursor.
+static mousegrid_cursor_t mousegrid_cursor;
+// If non-zero, an animation (visual feedback) of cursor bounds is active.
+static uint16_t animation_since;
+// If non-zero, an animation (visual feedback) of cursor bounds is active.
+static uint16_t alphabet_hook_since;
+static bool alphabet_x;
+// How many cursors are saved
+static uint8_t undo_count;
+static mousegrid_cursor_t undo_buffer[MG_UNDO_DEPTH];
+
+const uint16_t alphabet[MG_ALPHABET_SIZE + 1] PROGMEM = {MG_ALPHABET, 0};
+
+mousegrid_cursor_t init_cursor(void) {
+ return (mousegrid_cursor_t) {
+ .x = 0.5,
+ .y = 0.5,
+ .scale_x = 1.0,
+ .scale_y = 1.0,
+ };
+}
+
+void adjust_position(float *position, int8_t delta, float step) {
+ *position = *position + step * (float)delta;
+ if (*position < 0) {
+ *position = 0;
+ }
+ if (*position > 1) {
+ *position = 1;
+ }
+}
+
+void rescale(float *scale, float ratio) {
+ *scale = *scale * ratio;
+ if (*scale < MG_MIN_SCALE) {
+ *scale = MG_MIN_SCALE;
+ }
+}
+
+mousegrid_cursor_t update(mousegrid_cursor_t cursor, int8_t dx, int8_t dy) {
+ adjust_position(&(cursor.x), dx, cursor.scale_x / MG_HORIZONTAL_GRID);
+ adjust_position(&(cursor.y), dy, cursor.scale_y / MG_VERTICAL_GRID);
+ rescale(&(cursor.scale_x), MG_RESCALE_MULTIPLIER / MG_HORIZONTAL_GRID);
+ rescale(&(cursor.scale_y), MG_RESCALE_MULTIPLIER / MG_VERTICAL_GRID);
+ return cursor;
+}
+
+void save_undo(mousegrid_cursor_t cursor) {
+ if (undo_count == MG_UNDO_DEPTH) {
+ return;
+ }
+ undo_buffer[undo_count++] = cursor;
+}
+
+mousegrid_cursor_t restore_undo(void) {
+ if (undo_count == 0) {
+ return init_cursor();
+ }
+ return undo_buffer[--undo_count];
+}
+
+void move(int8_t dx, int8_t dy) {
+ save_undo(mousegrid_cursor);
+ mousegrid_cursor = update(mousegrid_cursor, dx, dy);
+}
+
+uint32_t mousegrid_animate(uint32_t trigger_time, void *cb_arg) {
+ if (animation_since == 0) {
+ // Animation has been cancelled.
+ return 0;
+ }
+
+ uint8_t step = (timer_read() - animation_since) / MG_ANIMATION_STEP;
+ int8_t dx, dy;
+ switch (step) {
+#if MG_ANIMATION == full
+ case 0:
+ case 8:
+ dx = 0;
+ dy = -1;
+ break;
+ case 1:
+ dx = 1;
+ dy = -1;
+ break;
+ case 2:
+ dx = 1;
+ dy = 0;
+ break;
+ case 3:
+ dx = 1;
+ dy = 1;
+ break;
+ case 4:
+ dx = 0;
+ dy = 1;
+ break;
+ case 5:
+ dx = -1;
+ dy = 1;
+ break;
+ case 6:
+ dx = -1;
+ dy = 0;
+ break;
+ case 7:
+ dx = -1;
+ dy = -1;
+ break;
+#elif MG_ANIMATION == corners
+ case 0:
+ dx = 1;
+ dy = -1;
+ break;
+ case 1:
+ dx = 1;
+ dy = 1;
+ break;
+ case 2:
+ dx = -1;
+ dy = 1;
+ break;
+ case 3:
+ dx = -1;
+ dy = -1;
+ break;
+#endif
+ default:
+ dx = 0;
+ dy = 0;
+ animation_since = 0;
+ break;
+ }
+ mousegrid_cursor_t animation_cursor = update(mousegrid_cursor, dx, dy);
+ digitizer_in_range_on();
+ digitizer_set_position(animation_cursor.x, animation_cursor.y);
+ return MG_ANIMATION_SLEEP;
+}
+
+void trigger_animation(void) {
+ animation_since = timer_read();
+ defer_exec(1, &mousegrid_animate, NULL);
+}
+
+void cancel_animation(void) {
+ animation_since = 0;
+}
+
+void send(void) {
+ digitizer_in_range_on();
+ digitizer_set_position(mousegrid_cursor.x, mousegrid_cursor.y);
+ uprintf(
+ "mousegrid to: %ux%u scale=%ux%u\n",
+ (int) (mousegrid_cursor.x * 100),
+ (int) (mousegrid_cursor.y * 100),
+ (int) (mousegrid_cursor.scale_x * 100),
+ (int) (mousegrid_cursor.scale_y * 100)
+ );
+}
+
+bool process_direction(uint16_t keycode) {
+ switch (keycode) {
+ case MG_TL:
+ move(-1, -1);
+ break;
+ case MG_T:
+ move(0, -1);
+ break;
+ case MG_TR:
+ move(1, -1);
+ break;
+ case MG_L:
+ move(-1, 0);
+ break;
+ case MG_C:
+ move(0, 0);
+ break;
+ case MG_R:
+ move(1, 0);
+ break;
+ case MG_BL:
+ move(-1, 1);
+ break;
+ case MG_B:
+ move(0, 1);
+ break;
+ case MG_BR:
+ move(1, 1);
+ break;
+ default:
+ return false;
+ }
+ cancel_animation();
+ send();
+ return true;
+}
+
+void trigger_alphabet(void) {
+ alphabet_hook_since = timer_read();
+ alphabet_x = true;
+}
+
+bool process_alphabet(keyrecord_t *record) {
+ // Most of the time we don't need to steal keycodes.
+ if (alphabet_hook_since == 0) {
+ return false;
+ }
+ // Has the hook expired?
+ if (alphabet_hook_since < timer_read() - MG_ALPHABET_HOOK_TIMEOUT) {
+ alphabet_hook_since = 0;
+ return false;
+ }
+ // Only key down events.
+ if (!record->event.pressed) {
+ return false;
+ }
+
+ keypos_t pos = record->event.key;
+ uint16_t keycode = keycode_at_keymap_location(MG_ALPHABET_LAYER, pos.row, pos.col);
+ int8_t index = 0;
+ for (;; ++index) {
+ uint16_t character = pgm_read_word(&alphabet[index]);
+ if (character == 0) {
+ // End of array.
+ return false;
+ }
+ if (character == keycode) {
+ break;
+ }
+ }
+
+ mousegrid_cursor_t cursor = mousegrid_cursor;
+ if (alphabet_x) {
+ cursor.x = (float)index * 1.0 / (MG_ALPHABET_SIZE - 1);
+ cursor.scale_x = MG_ALPHABET_RESCALE_MULTIPLIER / MG_ALPHABET_SIZE;
+ alphabet_x = false;
+ } else {
+ cursor.y = (float)index * 1.0 / (MG_ALPHABET_SIZE - 1);
+ cursor.scale_y = MG_ALPHABET_RESCALE_MULTIPLIER / MG_ALPHABET_SIZE;
+ alphabet_hook_since = 0;
+ }
+ mousegrid_cursor = cursor;
+ send();
+
+ return true;
+}
+
+bool process_command(uint16_t keycode) {
+ switch (keycode) {
+ case MG_ANIM:
+ trigger_animation();
+ return true;
+
+ case MG_UNDO:
+ mousegrid_cursor = restore_undo();
+ send();
+ return true;
+
+ case MG_ABC:
+ mousegrid_init();
+ trigger_alphabet();
+ send();
+ return true;
+
+ case MG_NEAR:
+ mousegrid_cursor.scale_x = MG_LOCAL_SCALE;
+ mousegrid_cursor.scale_y = MG_LOCAL_SCALE;
+ return true;
+
+ case MG_RST:
+ mousegrid_init();
+ send();
+ return true;
+ }
+
+ return false;
+}
+
+void mousegrid_init(void) {
+ mousegrid_cursor = init_cursor();
+ undo_count = 0;
+}
+
+bool process_mousegrid(uint16_t keycode, keyrecord_t *record) {
+ if (process_alphabet(record)) {
+ return false;
+ }
+
+ if (record->event.pressed) {
+ if (process_command(keycode)) {
+ return false;
+ }
+ if (process_direction(keycode)) {
+ return false;
+ }
+ }
+
+ if (IS_MOUSEGRID_KEYCODE(keycode)) {
+ return false;
+ }
+
+ return true;
+}
+
diff --git a/quantum/mousegrid.h b/quantum/mousegrid.h
new file mode 100644
index 000000000000..64f15aedef6a
--- /dev/null
+++ b/quantum/mousegrid.h
@@ -0,0 +1,72 @@
+#pragma once
+
+#include
+
+#include "action.h"
+
+// Directional grid of 3x3 by default. But 2x2 or 3x2 should also work.
+#ifndef MG_HORIZONTAL_GRID
+# define MG_HORIZONTAL_GRID 3
+#endif
+#ifndef MG_VERTICAL_GRID
+# define MG_VERTICAL_GRID 3
+#endif
+
+// Stop zooming in once we reach this scale.
+#ifndef MG_MIN_SCALE
+# define MG_MIN_SCALE 0.01
+#endif
+// Scale to set for MG_NEAR.
+#ifndef MG_LOCAL_SCALE
+# define MG_LOCAL_SCALE 0.2
+#endif
+
+// Rather than to zoom in to precisely 1/3 per dimension, leave a little bit of
+// overlap to be more forgiving of inaccuracies where the optimal path is
+// ambiguous.
+#ifndef MG_RESCALE_MULTIPLIER
+# define MG_RESCALE_MULTIPLIER (1.0 + (1.0 / 9.0))
+#endif
+
+// Which layer to use for the alphabet key lookups.
+#ifndef MG_ALPHABET_LAYER
+# define MG_ALPHABET_LAYER 0
+#endif
+// Default to English A-Z. Must exactly match the keycodes (eg: taps, etc).
+#ifndef MG_ALPHABET
+# define MG_ALPHABET \
+ KC_A, KC_B, KC_C, KC_D, KC_E, KC_F, KC_G, KC_H, KC_I, KC_J, KC_K, \
+ KC_L, KC_M, KC_N, KC_O, KC_P, KC_Q, KC_R, KC_S, KC_T, KC_U, KC_V, \
+ KC_W, KC_X, KC_Y, KC_Z
+# define MG_ALPHABET_SIZE 26
+#endif
+// Little bit of overlap when using directional grid to further refine cursor
+// position.
+#ifndef MG_ALPHABET_RESCALE_MULTIPLIER
+# define MG_ALPHABET_RESCALE_MULTIPLIER 2.0
+#endif
+#ifndef MG_ALPHABET_HOOK_TIMEOUT
+# define MG_ALPHABET_HOOK_TIMEOUT 5000
+#endif
+
+// Either "full" or "corners"
+#ifndef MG_ANIMATION
+# define MG_ANIMATION full
+#endif
+// Shorter step appeared to cause reports to be dropped or ignored.
+#define MG_ANIMATION_STEP 75
+
+#ifndef MG_UNDO_DEPTH
+# define MG_UNDO_DEPTH 6
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void mousegrid_init(void);
+bool process_mousegrid(uint16_t keycode, keyrecord_t *record);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/quantum/quantum.c b/quantum/quantum.c
index d4ebd58e7fca..f34962a6fd30 100644
--- a/quantum/quantum.c
+++ b/quantum/quantum.c
@@ -84,6 +84,10 @@
# include "process_layer_lock.h"
#endif
+#ifdef MOUSEGRID_ENABLE
+# include "mousegrid.h"
+#endif
+
#ifdef AUDIO_ENABLE
# ifndef GOODBYE_SONG
# define GOODBYE_SONG SONG(GOODBYE_SOUND)
@@ -416,6 +420,9 @@ bool process_record_quantum(keyrecord_t *record) {
#endif
#ifdef BLUETOOTH_ENABLE
process_connection(keycode, record) &&
+#endif
+#ifdef MOUSEGRID_ENABLE
+ process_mousegrid(keycode, record) &&
#endif
true)) {
return false;