summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStephen Ostermiller <stephenostermiller@users.noreply.github.com>2025-11-23 06:32:36 -0500
committerGitHub <noreply@github.com>2025-11-23 22:32:36 +1100
commit1a954e8da5dcbd81eeccb9d6ac41b6eda64d7b85 (patch)
tree4f09599d467a9c9bbb1dde722511139fa965ff03
parentc7e17538eea98540dbee71de5d024db99f7786fe (diff)
Reduce tap dance memory usage, move state out of data (#25415)
* Use less tap dance memory. Use dynamically allocated sparse array for tap dance state, dynamically allocate tap dance state when needed and free it when the tap dance is done. * new approach * Use null, check for null * Reformat with docker * Use uint8 with idx rather than uint16 with keycode in state * fix accidental change * reformat * Add null check * add documentation tip suggested by tzarc * Only allow tap dance state allocation on key down, not on key up Co-authored-by: Sergey Vlasov <sigprof@gmail.com> * Only allow tap dance allocation on key down, not on key up Co-authored-by: Sergey Vlasov <sigprof@gmail.com> * add user action required section --------- Co-authored-by: Sergey Vlasov <sigprof@gmail.com>
-rw-r--r--docs/ChangeLog/20250831/pr25415.md47
-rw-r--r--docs/features/tap_dance.md12
-rw-r--r--quantum/process_keycode/process_tap_dance.c122
-rw-r--r--quantum/process_keycode/process_tap_dance.h11
-rw-r--r--tests/tap_dance/examples.c4
5 files changed, 152 insertions, 44 deletions
diff --git a/docs/ChangeLog/20250831/pr25415.md b/docs/ChangeLog/20250831/pr25415.md
new file mode 100644
index 0000000000..54eb8c737e
--- /dev/null
+++ b/docs/ChangeLog/20250831/pr25415.md
@@ -0,0 +1,47 @@
1# Tap dance state removed from `tap_dance_action_t`
2
3The tap dance state has been separated from the action structure. Custom tap dance functions now receive the state as a separate parameter instead of accessing it through `action->state`.
4
5## User Action Required
6
7If your keymap uses custom tap dance functions that access the tap dance state, you need to update your code.
8
9 - You can't use `action->state`. Instead you need to call `tap_dance_state_t *tap_dance_get_state(uint8_t tap_dance_idx)` to get the state.
10 - You now get a pointer to the state, so use `->` notation rather than `.` notation to get fields from it.
11
12### Before:
13```c
14bool process_record_user(uint16_t keycode, keyrecord_t *record) {
15 tap_dance_action_t *action;
16
17 switch (keycode) {
18 case TD(CT_CLN):
19 action = tap_dance_get(QK_TAP_DANCE_GET_INDEX(keycode));
20 if (!record->event.pressed && action->state.count && !action->state.finished) {
21 tap_dance_tap_hold_t *tap_hold = (tap_dance_tap_hold_t *)action->user_data;
22 tap_code16(tap_hold->tap);
23 }
24
25 }
26 return true;
27}
28```
29
30### After:
31```c
32bool process_record_user(uint16_t keycode, keyrecord_t *record) {
33 tap_dance_action_t *action;
34 tap_dance_state_t* state;
35
36 switch (keycode) {
37 case TD(CT_CLN):
38 action = tap_dance_get(QK_TAP_DANCE_GET_INDEX(keycode));
39 state = tap_dance_get_state(QK_TAP_DANCE_GET_INDEX(keycode));
40 if (!record->event.pressed && state != NULL && state->count && !state->finished) {
41 tap_dance_tap_hold_t *tap_hold = (tap_dance_tap_hold_t *)action->user_data;
42 tap_code16(tap_hold->tap);
43 }
44 }
45 return true;
46}
47```
diff --git a/docs/features/tap_dance.md b/docs/features/tap_dance.md
index d533e41aaa..688241a16b 100644
--- a/docs/features/tap_dance.md
+++ b/docs/features/tap_dance.md
@@ -40,6 +40,10 @@ Similar to the first option, the second and third option are good for simple lay
40 40
41For more complicated cases, like blink the LEDs, fiddle with the backlighting, and so on, use the fourth or fifth option. Examples of each are listed below. 41For more complicated cases, like blink the LEDs, fiddle with the backlighting, and so on, use the fourth or fifth option. Examples of each are listed below.
42 42
43::: tip
44If too many tap dances are active at the same time, later ones won't have any effect. You need to increase `TAP_DANCE_MAX_SIMULTANEOUS` by adding `#define TAP_DANCE_MAX_SIMULTANEOUS 5` (or higher) to your keymap's `config.h` file if you expect that users may hold down many tap dance keys simultaneously. By default, only 3 tap dance keys can be used together at the same time.
45:::
46
43## Implementation Details {#implementation} 47## Implementation Details {#implementation}
44 48
45Well, that's the bulk of it! You should now be able to work through the examples below, and to develop your own Tap Dance functionality. But if you want a deeper understanding of what's going on behind the scenes, then read on for the explanation of how it all works! 49Well, that's the bulk of it! You should now be able to work through the examples below, and to develop your own Tap Dance functionality. But if you want a deeper understanding of what's going on behind the scenes, then read on for the explanation of how it all works!
@@ -209,11 +213,13 @@ tap_dance_action_t tap_dance_actions[] = {
209 213
210bool process_record_user(uint16_t keycode, keyrecord_t *record) { 214bool process_record_user(uint16_t keycode, keyrecord_t *record) {
211 tap_dance_action_t *action; 215 tap_dance_action_t *action;
216 tap_dance_state_t* state;
212 217
213 switch (keycode) { 218 switch (keycode) {
214 case TD(CT_CLN): // list all tap dance keycodes with tap-hold configurations 219 case TD(CT_CLN):
215 action = &tap_dance_actions[QK_TAP_DANCE_GET_INDEX(keycode)]; 220 action = tap_dance_get(QK_TAP_DANCE_GET_INDEX(keycode));
216 if (!record->event.pressed && action->state.count && !action->state.finished) { 221 state = tap_dance_get_state(QK_TAP_DANCE_GET_INDEX(keycode));
222 if (!record->event.pressed && state != NULL && state->count && !state->finished) {
217 tap_dance_tap_hold_t *tap_hold = (tap_dance_tap_hold_t *)action->user_data; 223 tap_dance_tap_hold_t *tap_hold = (tap_dance_tap_hold_t *)action->user_data;
218 tap_code16(tap_hold->tap); 224 tap_code16(tap_hold->tap);
219 } 225 }
diff --git a/quantum/process_keycode/process_tap_dance.c b/quantum/process_keycode/process_tap_dance.c
index 11df62763d..36a94d8d28 100644
--- a/quantum/process_keycode/process_tap_dance.c
+++ b/quantum/process_keycode/process_tap_dance.c
@@ -24,8 +24,46 @@
24#include "keymap_introspection.h" 24#include "keymap_introspection.h"
25 25
26static uint16_t active_td; 26static uint16_t active_td;
27
28#ifndef TAP_DANCE_MAX_SIMULTANEOUS
29# define TAP_DANCE_MAX_SIMULTANEOUS 3
30#endif
31
32static tap_dance_state_t tap_dance_states[TAP_DANCE_MAX_SIMULTANEOUS];
33
27static uint16_t last_tap_time; 34static uint16_t last_tap_time;
28 35
36static tap_dance_state_t *tap_dance_get_or_allocate_state(uint8_t tap_dance_idx, bool allocate) {
37 uint8_t i;
38 if (tap_dance_idx >= tap_dance_count()) {
39 return NULL;
40 }
41 // Search for a state already used for this keycode
42 for (i = 0; i < TAP_DANCE_MAX_SIMULTANEOUS; i++) {
43 if (tap_dance_states[i].in_use && tap_dance_states[i].index == tap_dance_idx) {
44 return &tap_dance_states[i];
45 }
46 }
47 // No existing state found; bail out if new state allocation is not allowed
48 if (!allocate) {
49 return NULL;
50 }
51 // Search for the first available state
52 for (i = 0; i < TAP_DANCE_MAX_SIMULTANEOUS; i++) {
53 if (!tap_dance_states[i].in_use) {
54 tap_dance_states[i].index = tap_dance_idx;
55 tap_dance_states[i].in_use = true;
56 return &tap_dance_states[i];
57 }
58 }
59 // No states are available, tap dance won't happen
60 return NULL;
61}
62
63tap_dance_state_t *tap_dance_get_state(uint8_t tap_dance_idx) {
64 return tap_dance_get_or_allocate_state(tap_dance_idx, false);
65}
66
29void tap_dance_pair_on_each_tap(tap_dance_state_t *state, void *user_data) { 67void tap_dance_pair_on_each_tap(tap_dance_state_t *state, void *user_data) {
30 tap_dance_pair_t *pair = (tap_dance_pair_t *)user_data; 68 tap_dance_pair_t *pair = (tap_dance_pair_t *)user_data;
31 69
@@ -86,58 +124,64 @@ static inline void _process_tap_dance_action_fn(tap_dance_state_t *state, void *
86 } 124 }
87} 125}
88 126
89static inline void process_tap_dance_action_on_each_tap(tap_dance_action_t *action) { 127static inline void process_tap_dance_action_on_each_tap(tap_dance_action_t *action, tap_dance_state_t *state) {
90 action->state.count++; 128 state->count++;
91 action->state.weak_mods = get_mods(); 129 state->weak_mods = get_mods();
92 action->state.weak_mods |= get_weak_mods(); 130 state->weak_mods |= get_weak_mods();
93#ifndef NO_ACTION_ONESHOT 131#ifndef NO_ACTION_ONESHOT
94 action->state.oneshot_mods = get_oneshot_mods(); 132 state->oneshot_mods = get_oneshot_mods();
95#endif 133#endif
96 _process_tap_dance_action_fn(&action->state, action->user_data, action->fn.on_each_tap); 134 _process_tap_dance_action_fn(state, action->user_data, action->fn.on_each_tap);
97} 135}
98 136
99static inline void process_tap_dance_action_on_each_release(tap_dance_action_t *action) { 137static inline void process_tap_dance_action_on_each_release(tap_dance_action_t *action, tap_dance_state_t *state) {
100 _process_tap_dance_action_fn(&action->state, action->user_data, action->fn.on_each_release); 138 _process_tap_dance_action_fn(state, action->user_data, action->fn.on_each_release);
101} 139}
102 140
103static inline void process_tap_dance_action_on_reset(tap_dance_action_t *action) { 141static inline void process_tap_dance_action_on_reset(tap_dance_action_t *action, tap_dance_state_t *state) {
104 _process_tap_dance_action_fn(&action->state, action->user_data, action->fn.on_reset); 142 _process_tap_dance_action_fn(state, action->user_data, action->fn.on_reset);
105 del_weak_mods(action->state.weak_mods); 143 del_weak_mods(state->weak_mods);
106#ifndef NO_ACTION_ONESHOT 144#ifndef NO_ACTION_ONESHOT
107 del_mods(action->state.oneshot_mods); 145 del_mods(state->oneshot_mods);
108#endif 146#endif
109 send_keyboard_report(); 147 send_keyboard_report();
110 action->state = (const tap_dance_state_t){0}; 148 // Clear the tap dance state and mark it as unused
149 memset(state, 0, sizeof(tap_dance_state_t));
111} 150}
112 151
113static inline void process_tap_dance_action_on_dance_finished(tap_dance_action_t *action) { 152static inline void process_tap_dance_action_on_dance_finished(tap_dance_action_t *action, tap_dance_state_t *state) {
114 if (!action->state.finished) { 153 if (!state->finished) {
115 action->state.finished = true; 154 state->finished = true;
116 add_weak_mods(action->state.weak_mods); 155 add_weak_mods(state->weak_mods);
117#ifndef NO_ACTION_ONESHOT 156#ifndef NO_ACTION_ONESHOT
118 add_mods(action->state.oneshot_mods); 157 add_mods(state->oneshot_mods);
119#endif 158#endif
120 send_keyboard_report(); 159 send_keyboard_report();
121 _process_tap_dance_action_fn(&action->state, action->user_data, action->fn.on_dance_finished); 160 _process_tap_dance_action_fn(state, action->user_data, action->fn.on_dance_finished);
122 } 161 }
123 active_td = 0; 162 active_td = 0;
124 if (!action->state.pressed) { 163 if (!state->pressed) {
125 // There will not be a key release event, so reset now. 164 // There will not be a key release event, so reset now.
126 process_tap_dance_action_on_reset(action); 165 process_tap_dance_action_on_reset(action, state);
127 } 166 }
128} 167}
129 168
130bool preprocess_tap_dance(uint16_t keycode, keyrecord_t *record) { 169bool preprocess_tap_dance(uint16_t keycode, keyrecord_t *record) {
131 tap_dance_action_t *action; 170 tap_dance_action_t *action;
171 tap_dance_state_t * state;
132 172
133 if (!record->event.pressed) return false; 173 if (!record->event.pressed) return false;
134 174
135 if (!active_td || keycode == active_td) return false; 175 if (!active_td || keycode == active_td) return false;
136 176
137 action = tap_dance_get(QK_TAP_DANCE_GET_INDEX(active_td)); 177 action = tap_dance_get(QK_TAP_DANCE_GET_INDEX(active_td));
138 action->state.interrupted = true; 178 state = tap_dance_get_state(QK_TAP_DANCE_GET_INDEX(active_td));
139 action->state.interrupting_keycode = keycode; 179 if (state == NULL) {
140 process_tap_dance_action_on_dance_finished(action); 180 return false;
181 }
182 state->interrupted = true;
183 state->interrupting_keycode = keycode;
184 process_tap_dance_action_on_dance_finished(action, state);
141 185
142 // Tap dance actions can leave some weak mods active (e.g., if the tap dance is mapped to a keycode with 186 // Tap dance actions can leave some weak mods active (e.g., if the tap dance is mapped to a keycode with
143 // modifiers), but these weak mods should not affect the keypress which interrupted the tap dance. 187 // modifiers), but these weak mods should not affect the keypress which interrupted the tap dance.
@@ -151,8 +195,9 @@ bool preprocess_tap_dance(uint16_t keycode, keyrecord_t *record) {
151} 195}
152 196
153bool process_tap_dance(uint16_t keycode, keyrecord_t *record) { 197bool process_tap_dance(uint16_t keycode, keyrecord_t *record) {
154 int td_index; 198 uint8_t td_index;
155 tap_dance_action_t *action; 199 tap_dance_action_t *action;
200 tap_dance_state_t * state;
156 201
157 switch (keycode) { 202 switch (keycode) {
158 case QK_TAP_DANCE ... QK_TAP_DANCE_MAX: 203 case QK_TAP_DANCE ... QK_TAP_DANCE_MAX:
@@ -161,16 +206,19 @@ bool process_tap_dance(uint16_t keycode, keyrecord_t *record) {
161 return false; 206 return false;
162 } 207 }
163 action = tap_dance_get(td_index); 208 action = tap_dance_get(td_index);
164 209 state = tap_dance_get_or_allocate_state(td_index, record->event.pressed);
165 action->state.pressed = record->event.pressed; 210 if (state == NULL) {
211 return false;
212 }
213 state->pressed = record->event.pressed;
166 if (record->event.pressed) { 214 if (record->event.pressed) {
167 last_tap_time = timer_read(); 215 last_tap_time = timer_read();
168 process_tap_dance_action_on_each_tap(action); 216 process_tap_dance_action_on_each_tap(action, state);
169 active_td = action->state.finished ? 0 : keycode; 217 active_td = state->finished ? 0 : keycode;
170 } else { 218 } else {
171 process_tap_dance_action_on_each_release(action); 219 process_tap_dance_action_on_each_release(action, state);
172 if (action->state.finished) { 220 if (state->finished) {
173 process_tap_dance_action_on_reset(action); 221 process_tap_dance_action_on_reset(action, state);
174 if (active_td == keycode) { 222 if (active_td == keycode) {
175 active_td = 0; 223 active_td = 0;
176 } 224 }
@@ -185,16 +233,18 @@ bool process_tap_dance(uint16_t keycode, keyrecord_t *record) {
185 233
186void tap_dance_task(void) { 234void tap_dance_task(void) {
187 tap_dance_action_t *action; 235 tap_dance_action_t *action;
236 tap_dance_state_t * state;
188 237
189 if (!active_td || timer_elapsed(last_tap_time) <= GET_TAPPING_TERM(active_td, &(keyrecord_t){})) return; 238 if (!active_td || timer_elapsed(last_tap_time) <= GET_TAPPING_TERM(active_td, &(keyrecord_t){})) return;
190 239
191 action = tap_dance_get(QK_TAP_DANCE_GET_INDEX(active_td)); 240 action = tap_dance_get(QK_TAP_DANCE_GET_INDEX(active_td));
192 if (!action->state.interrupted) { 241 state = tap_dance_get_state(QK_TAP_DANCE_GET_INDEX(active_td));
193 process_tap_dance_action_on_dance_finished(action); 242 if (state != NULL && !state->interrupted) {
243 process_tap_dance_action_on_dance_finished(action, state);
194 } 244 }
195} 245}
196 246
197void reset_tap_dance(tap_dance_state_t *state) { 247void reset_tap_dance(tap_dance_state_t *state) {
198 active_td = 0; 248 active_td = 0;
199 process_tap_dance_action_on_reset((tap_dance_action_t *)state); 249 process_tap_dance_action_on_reset(tap_dance_get(state->index), state);
200} 250}
diff --git a/quantum/process_keycode/process_tap_dance.h b/quantum/process_keycode/process_tap_dance.h
index 5cccbdf439..5a972cee5a 100644
--- a/quantum/process_keycode/process_tap_dance.h
+++ b/quantum/process_keycode/process_tap_dance.h
@@ -28,15 +28,16 @@ typedef struct {
28#ifndef NO_ACTION_ONESHOT 28#ifndef NO_ACTION_ONESHOT
29 uint8_t oneshot_mods; 29 uint8_t oneshot_mods;
30#endif 30#endif
31 bool pressed : 1; 31 bool pressed : 1;
32 bool finished : 1; 32 bool finished : 1;
33 bool interrupted : 1; 33 bool interrupted : 1;
34 bool in_use : 1;
35 uint8_t index;
34} tap_dance_state_t; 36} tap_dance_state_t;
35 37
36typedef void (*tap_dance_user_fn_t)(tap_dance_state_t *state, void *user_data); 38typedef void (*tap_dance_user_fn_t)(tap_dance_state_t *state, void *user_data);
37 39
38typedef struct tap_dance_action_t { 40typedef struct tap_dance_action_t {
39 tap_dance_state_t state;
40 struct { 41 struct {
41 tap_dance_user_fn_t on_each_tap; 42 tap_dance_user_fn_t on_each_tap;
42 tap_dance_user_fn_t on_dance_finished; 43 tap_dance_user_fn_t on_dance_finished;
@@ -80,6 +81,8 @@ typedef struct {
80 81
81void reset_tap_dance(tap_dance_state_t *state); 82void reset_tap_dance(tap_dance_state_t *state);
82 83
84tap_dance_state_t *tap_dance_get_state(uint8_t tap_dance_idx);
85
83/* To be used internally */ 86/* To be used internally */
84 87
85bool preprocess_tap_dance(uint16_t keycode, keyrecord_t *record); 88bool preprocess_tap_dance(uint16_t keycode, keyrecord_t *record);
diff --git a/tests/tap_dance/examples.c b/tests/tap_dance/examples.c
index 4b6bdb2090..6aaf008232 100644
--- a/tests/tap_dance/examples.c
+++ b/tests/tap_dance/examples.c
@@ -81,11 +81,13 @@ typedef struct {
81 81
82bool process_record_user(uint16_t keycode, keyrecord_t *record) { 82bool process_record_user(uint16_t keycode, keyrecord_t *record) {
83 tap_dance_action_t *action; 83 tap_dance_action_t *action;
84 tap_dance_state_t* state;
84 85
85 switch (keycode) { 86 switch (keycode) {
86 case TD(CT_CLN): 87 case TD(CT_CLN):
87 action = tap_dance_get(QK_TAP_DANCE_GET_INDEX(keycode)); 88 action = tap_dance_get(QK_TAP_DANCE_GET_INDEX(keycode));
88 if (!record->event.pressed && action->state.count && !action->state.finished) { 89 state = tap_dance_get_state(QK_TAP_DANCE_GET_INDEX(keycode));
90 if (!record->event.pressed && state != NULL && state->count && !state->finished) {
89 tap_dance_tap_hold_t *tap_hold = (tap_dance_tap_hold_t *)action->user_data; 91 tap_dance_tap_hold_t *tap_hold = (tap_dance_tap_hold_t *)action->user_data;
90 tap_code16(tap_hold->tap); 92 tap_code16(tap_hold->tap);
91 } 93 }