Merge pull request #383 from clangen/clangen/input-latency

Lower latency input handling, especially with respect to mouse input.
This commit is contained in:
casey langen 2020-11-17 14:02:46 -08:00 committed by GitHub
commit 7731b3573a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 148 additions and 11 deletions

View File

@ -1,3 +1,9 @@
0.96.0
* improved input latency, especially related to mouse events.
--------------------------------------------------------------------------------
0.95.0
* added support for searching by regex; press `M-m` in track or filter views to

View File

@ -87,7 +87,7 @@ musikcube would not be possible without the following excellent free, open sourc
| [boost](http://www.boost.org/) | [flac](https://xiph.org/flac/) | [alsa](https://www.alsa-project.org) | [taglib](http://taglib.org/) | [websocketpp](https://github.com/zaphoyd/websocketpp) | [rxjava](https://github.com/ReactiveX/RxJava) | [ncurses](https://www.gnu.org/software/ncurses/) |
| [sqlite](https://www.sqlite.org/) | [ogg/vorbis](http://www.vorbis.com/) | [pulseaudio](https://www.freedesktop.org/wiki/Software/PulseAudio/) | [glide](https://github.com/bumptech/glide) | [libmicrohttpd](https://www.gnu.org/software/libmicrohttpd/) | [rxandroid](https://github.com/ReactiveX/RxAndroid) | [pdcurses (win32a variant)](https://www.projectpluto.com/win32a.htm) |
| [utfcpp](https://github.com/nemtrif/utfcpp) | [mad](http://www.underbit.com/products/mad/) + [nomad](https://github.com/cmus/cmus/tree/master/ip) | [core audio](https://developer.apple.com/library/content/documentation/MusicAudio/Conceptual/CoreAudioOverview/Introduction/Introduction.html) | | [libcurl](https://curl.haxx.se/libcurl/) | [stetho](http://facebook.github.io/stetho/) | [recycler-fast-scroll](https://github.com/plusCubed/recycler-fast-scroll) |
| [nlohmann json](https://github.com/nlohmann/json) | [faad2](http://www.audiocoding.com/faad2.html) | [wasapi](https://msdn.microsoft.com/en-us/library/windows/desktop/dd371455(v=vs.85).aspx) | | [libressl](https://www.libressl.org/) | [fabric](http://fabric.io) | |
| [nlohmann json](https://github.com/nlohmann/json) | [faad2](http://www.audiocoding.com/faad2.html) | [wasapi](https://msdn.microsoft.com/en-us/library/windows/desktop/dd371455(v=vs.85).aspx) | | [libressl](https://www.libressl.org/) | [fabric](http://fabric.io) | |
| [kissfft](http://kissfft.sourceforge.net/) | [exoplayer](https://github.com/google/ExoPlayer) | [directsound](https://msdn.microsoft.com/en-us/library/windows/desktop/ee416960(v=vs.85).aspx) | | [nv-websocket-client](https://github.com/TakahikoKawasaki/nv-websocket-client) | [AndroidVideoCache](https://github.com/danikula/AndroidVideoCache) | |
| [sigslot](http://sigslot.sourceforge.net/) | | [waveout](https://msdn.microsoft.com/en-us/library/windows/desktop/dd743876(v=vs.85).aspx) | | [okhttp](http://square.github.io/okhttp/) | | |
| [wcwidth.c](http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c) | | | | | | |

View File

@ -112,6 +112,125 @@ static bool isLangUtf8() {
}
#endif
/* the way curses represents mouse button state is really gross, and makes it
annoying to process click events with low latency while also supporting double
clicks and multiple buttons. this structure is used to maintain and manipulate
state related to mouse buttons, and is used by the main loop of the app to
generate button click events for Window instances to handle. beware: kludge
and hacks below... */
struct MouseButtonState {
bool Update(const MEVENT& rawEvent) {
const int state = rawEvent.bstate;
const int x = rawEvent.x;
const int y = rawEvent.y;
bool result = false;
const int64_t newTime = App::Now();
/* ugh... */
const bool isDown = state & BUTTON1_PRESSED || state & BUTTON2_PRESSED || state & BUTTON3_PRESSED;
const bool isUp = state & BUTTON1_RELEASED || state & BUTTON2_RELEASED || state & BUTTON3_RELEASED;
if (!isDown && !isUp) {
/* if both of these were false we got an unexpected mouse event. let's
go ahead and reset our state and return */
Reset();
return false;
}
int newButton = 0;
if (state & BUTTON1_PRESSED || state & BUTTON1_RELEASED) {
newButton = 1;
}
if (state & BUTTON2_PRESSED || state & BUTTON2_RELEASED) {
newButton = 2;
}
if (state & BUTTON3_PRESSED || state & BUTTON3_RELEASED) {
newButton = 3;
}
/* threshold for double click, in milliseconds*/
const bool elapsed = newTime - time > 300;
if ((!elapsed && lastX >= 0 && lastY >= 0) && (std::abs(x - lastX) > 2 || std::abs(y - lastY) > 2)) {
/* cursor traveled too far, reset state and convert back to a single click */
this->Reset();
}
if (newButton != button || elapsed) {
/* if the button changed or there was too much time between state
changes, go ahead and reset now */
Reset();
}
if (isDown) {
/* to improve latency response we treat the leading edge of the first
click as the event, and don't wait for a "release" event, because that
can take hundreds of milliseconds. we kludge the `wasDown` flag to be
false, so the subsequent click (below) can be "correctly" recorded
as a double click. */
if (!this->clicked) {
this->clicked = true;
this->wasDown = false;
}
else {
this->wasDown = true;
}
result = true;
}
else if (clicked && wasDown && !isDown) {
this->doubleClicked = true;
this->wasDown = true;
result = true;
}
this->button = newButton;
this->time = newTime;
this->lastX = x;
this->lastY = y;
return result;
}
void Reset() noexcept {
button = 0;
state = 0;
time = 0;
lastX = -1;
lastY = -1;
wasDown = false;
clicked = false;
doubleClicked = false;
}
mmask_t ToCursesState() noexcept {
if (doubleClicked) {
switch (button) {
case 1: return BUTTON1_DOUBLE_CLICKED;
case 2: return BUTTON2_DOUBLE_CLICKED;
case 3: return BUTTON3_DOUBLE_CLICKED;
default: break;
}
}
else if (clicked) {
switch (button) {
case 1: return BUTTON1_CLICKED;
case 2: return BUTTON2_CLICKED;
case 3: return BUTTON3_CLICKED;
default: break;
}
}
return 0;
}
bool wasDown{ false };
bool clicked{ false };
bool doubleClicked{ false };
int button;
int state;
int lastX{ -1 }, lastY{ -1 };
int64_t time{ -1 };
};
App& App::Instance() {
if (!instance) {
throw std::runtime_error("app not running!");
@ -158,6 +277,16 @@ void App::InitCurses() {
refresh();
curs_set(0);
mousemask(ALL_MOUSE_EVENTS, nullptr);
mouseinterval(0);
#ifndef WIN32
set_escdelay(20);
timeout(IDLE_TIMEOUT_MS);
#endif
#ifdef WIN32
nodelay(stdscr, true);
#endif
#ifdef WIN32
PDC_set_function_key(FUNCTION_KEY_SHUT_DOWN, 4);
@ -172,8 +301,6 @@ void App::InitCurses() {
this->SetIcon(this->iconId);
}
#endif
#else
set_escdelay(20);
#endif
Colors::Init(this->colorMode, this->bgType);
@ -378,7 +505,6 @@ void App::Run(ILayoutPtr layout) {
}
#endif
MEVENT mouseEvent;
int64_t ch;
std::string kn;
@ -387,6 +513,9 @@ void App::Run(ILayoutPtr layout) {
this->ChangeLayout(layout);
MEVENT rawMouseEvent;
MouseButtonState mouseButtonState;
while (!this->quit && !disconnected) {
kn = "";
@ -397,13 +526,14 @@ void App::Run(ILayoutPtr layout) {
goto process;
}
timeout(IDLE_TIMEOUT_MS);
if (this->state.input && this->state.focused->GetContent()) {
/* if the focused window is an input, allow it to draw a cursor */
WINDOW *c = this->state.focused->GetContent();
keypad(c, TRUE);
wtimeout(c, IDLE_TIMEOUT_MS);
#ifdef WIN32
nodelay(c, true);
#endif
ch = wgetch(c);
}
else {
@ -435,21 +565,22 @@ process:
}
else if (this->mouseEnabled && kn == "KEY_MOUSE") {
#ifdef WIN32
if (nc_getmouse(&mouseEvent) == 0) {
if (nc_getmouse(&rawMouseEvent) == 0) {
#else
if (getmouse(&mouseEvent) == 0) {
if (getmouse(&rawMouseEvent) == 0) {
#endif
auto active = this->state.ActiveLayout();
if (active) {
using Event = IMouseHandler::Event;
auto window = dynamic_cast<IWindow*>(active.get());
Event event(mouseEvent, window);
Event event(rawMouseEvent, window);
if (event.MouseWheelDown() || event.MouseWheelUp()) {
if (state.focused) {
state.focused->MouseEvent(event);
}
}
else {
else if (mouseButtonState.Update(rawMouseEvent)) {
event.state = mouseButtonState.ToCursesState();
active->MouseEvent(event);
}
}

View File

@ -40,7 +40,7 @@
#include <musikcore/runtime/IMessageQueue.h>
#ifdef WIN32
#define IDLE_TIMEOUT_MS 10
#define IDLE_TIMEOUT_MS 0
#define REDRAW_DEBOUNCE_MS 100
#else
#define IDLE_TIMEOUT_MS 75