fix(wayland): the first key repeat lands at the delay, not as a burst of the periods since the press
keyboard_key set lastFireTime to the press and TickKeyRepeats caught up on every period since it, so the first tick past the compositor's delay fired floor(delay / period) repeats at once: with rate 25 and delay 600 a held Backspace deleted sixteen characters at the delay mark and a held letter put fifteen copies in the field, from a hardware keyboard and from an on-screen keyboard that sends Backspace as key events. A rate above 1000 made the period zero and the loop endless. The first repeat now lands at the delay, as wl_keyboard.repeat_info promises (one after `delay`, then `rate` a second): the first tick past the delay sets lastFireTime to pressTime + delay - period. The period is at least a millisecond, and the catch-up after a stalled frame is capped (kKeyRepeatCatchUp, three) with the rest of the stall dropped, so a held key is never a burst of edits. TickKeyRepeats takes the clock as a parameter for the tests; the frame path's call is unchanged. tests/KeyRepeat drives TickKeyRepeats with an injected clock on a default-constructed Window: nothing before the delay, one at it, one per period after, a capped catch-up, a key without text, a rate above 1000, a released key, rate 0.
This commit is contained in:
parent
8c9a588083
commit
3f646ec42b
4 changed files with 145 additions and 11 deletions
|
|
@ -634,6 +634,7 @@ void Device::keyboard_key(void *data, wl_keyboard *keyboard, uint32_t serial, ui
|
|||
keyRepeat.utf8 = std::move(utf8);
|
||||
keyRepeat.pressTime = std::chrono::steady_clock::now();
|
||||
keyRepeat.lastFireTime = keyRepeat.pressTime;
|
||||
keyRepeat.fired = false;
|
||||
} else {
|
||||
keyboardWindow->heldKeys.erase(code);
|
||||
keyboardWindow->onRawKeyUp.Invoke(code);
|
||||
|
|
@ -659,28 +660,44 @@ void Device::keyboard_repeat_info(void *data, wl_keyboard *keyboard, int32_t rat
|
|||
}
|
||||
|
||||
void Device::TickKeyRepeats() {
|
||||
TickKeyRepeats(std::chrono::steady_clock::now());
|
||||
}
|
||||
|
||||
void Device::TickKeyRepeats(std::chrono::steady_clock::time_point now) {
|
||||
if (!keyRepeat.active || !keyboardWindow) return;
|
||||
if (keyRepeat.rate <= 0) return;
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
using ms = std::chrono::milliseconds;
|
||||
auto sincePress = std::chrono::duration_cast<ms>(now - keyRepeat.pressTime).count();
|
||||
if (sincePress < keyRepeat.delay) return;
|
||||
const auto delay = ms(keyRepeat.delay);
|
||||
if (now - keyRepeat.pressTime < delay) return;
|
||||
|
||||
auto period = std::chrono::milliseconds(1000 / keyRepeat.rate);
|
||||
auto sinceLastFire = std::chrono::duration_cast<ms>(now - keyRepeat.lastFireTime).count();
|
||||
if (sinceLastFire < period.count()) return;
|
||||
// At least a millisecond: a compositor's rate above 1000 would make the
|
||||
// period zero and the loop below endless.
|
||||
const auto period = ms(std::max(1, 1000 / keyRepeat.rate));
|
||||
|
||||
// Catch up — emit one event per missed period so a paused frame doesn't
|
||||
// make the repeat permanently lag behind.
|
||||
while (now - keyRepeat.lastFireTime >= period) {
|
||||
// The first repeat lands at the delay — wl_keyboard.repeat_info's
|
||||
// contract: one after `delay`, then `rate` a second — so the delay is not
|
||||
// counted as periods missed.
|
||||
if (!keyRepeat.fired) {
|
||||
keyRepeat.lastFireTime = keyRepeat.pressTime + delay - period;
|
||||
keyRepeat.fired = true;
|
||||
}
|
||||
if (now - keyRepeat.lastFireTime < period) return;
|
||||
|
||||
// Catch up — one event per missed period, so a paused frame doesn't make
|
||||
// the repeat permanently lag behind — but a few at most: a long stall is
|
||||
// not a burst of edits, the rest of it is dropped.
|
||||
int fires = 0;
|
||||
while (now - keyRepeat.lastFireTime >= period && fires < kKeyRepeatCatchUp) {
|
||||
keyboardWindow->onRawKeyDown.Invoke(keyRepeat.key);
|
||||
keyboardWindow->onRawKeyHold.Invoke(keyRepeat.key);
|
||||
if (!keyRepeat.utf8.empty()) {
|
||||
keyboardWindow->onTextInput.Invoke(keyRepeat.utf8);
|
||||
}
|
||||
keyRepeat.lastFireTime += period;
|
||||
++fires;
|
||||
}
|
||||
if (now - keyRepeat.lastFireTime >= period) keyRepeat.lastFireTime = now;
|
||||
}
|
||||
|
||||
void Device::ForgetTouches() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue