//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Key repeat as wl_keyboard.repeat_info promises it: one repeat after `delay` // milliseconds, then `rate` a second. The first implementation set the last // fire time to the press and let the catch-up loop count the delay as missed // periods, so the first repeat was a burst of floor(delay / period) events — // a held Backspace deleted a dozen characters at once — and a rate above 1000 // made the period zero and the loop endless. Drives Device::TickKeyRepeats // with an injected clock on a default-constructed Window (inert without a // compositor): no compositor, no keyboard needed. #include import Crafter.Graphics; import Crafter.Event; import std; using namespace Crafter; using namespace std::chrono_literals; namespace { int failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } using Clock = std::chrono::steady_clock; // A press of `key` at t0 with the compositor's rate and delay, as keyboard_key records it. void Press(KeyCode key, std::string utf8, int rate, int delay, Clock::time_point t0) { Device::keyRepeat.rate = rate; Device::keyRepeat.delay = delay; Device::keyRepeat.active = rate > 0; Device::keyRepeat.key = key; Device::keyRepeat.utf8 = std::move(utf8); Device::keyRepeat.pressTime = t0; Device::keyRepeat.lastFireTime = t0; Device::keyRepeat.fired = false; } } // namespace int main() { Window window; // default ctor creates no surface — safe without a compositor int downs = 0; int texts = 0; EventListener down(&window.onRawKeyDown, [&](KeyCode) { ++downs; }); EventListener text(&window.onTextInput, [&](const std::string_view) { ++texts; }); Device::keyboardWindow = &window; const Clock::time_point t0 = Clock::now(); // rate 25 (a 40 ms period), delay 600: wlroots' and KWin's defaults. Press(14, "a", 25, 600, t0); Device::TickKeyRepeats(t0 + 30ms); Device::TickKeyRepeats(t0 + 599ms); Check(downs == 0 && texts == 0, "nothing repeats before the delay"); Device::TickKeyRepeats(t0 + 600ms); Check(downs == 1 && texts == 1, "one repeat at the delay, not a burst of fifteen"); Device::TickKeyRepeats(t0 + 620ms); Check(downs == 1, "none before the next period"); Device::TickKeyRepeats(t0 + 640ms); Check(downs == 2, "one per period from there"); Device::TickKeyRepeats(t0 + 680ms); Device::TickKeyRepeats(t0 + 720ms); Check(downs == 4, "a tick per period fires once each"); // Frames stalled for a second: a few catch up, the rest of the stall is dropped. Device::TickKeyRepeats(t0 + 1720ms); Check(downs == 4 + kKeyRepeatCatchUp, "a stall catches up by a few repeats, not one per missed period"); Device::TickKeyRepeats(t0 + 1730ms); Check(downs == 4 + kKeyRepeatCatchUp, "and the stall's remainder is dropped"); Device::TickKeyRepeats(t0 + 1760ms); Check(downs == 5 + kKeyRepeatCatchUp, "the next period after the stall fires once"); // A key with no text (Backspace): the key event alone. downs = texts = 0; Press(14, "", 25, 500, t0); Device::TickKeyRepeats(t0 + 500ms); Check(downs == 1 && texts == 0, "a key without text repeats the key alone"); // A rate above 1000 a second: a period of a millisecond, no endless loop. downs = 0; Press(14, "", 2000, 100, t0); Device::TickKeyRepeats(t0 + 100ms); Check(downs == 1, "a rate above 1000 repeats once at the delay"); Device::TickKeyRepeats(t0 + 200ms); Check(downs == 1 + kKeyRepeatCatchUp, "and a tick late by many periods fires the cap, not forever"); // Released: nothing more. Device::keyRepeat.active = false; Device::TickKeyRepeats(t0 + 300ms); Check(downs == 1 + kKeyRepeatCatchUp, "a released key repeats no more"); // A compositor that disables repeat (rate 0). Press(14, "", 0, 100, t0); Device::TickKeyRepeats(t0 + 1000ms); Check(downs == 1 + kKeyRepeatCatchUp, "rate 0 is no repeat"); Device::keyboardWindow = nullptr; return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }