Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions include/pugl/pugl.h
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,34 @@ puglGrabFocus(PuglView* view);
PUGL_API bool
puglHasFocus(const PuglView* view);

/**
Return true if the event is wanted. Will either be a PUGL_TEXT or PUGL_KEY_*.
*/
typedef bool (*PuglKeyboardEventFilter)(PuglView* view, const PuglEvent* event);

/**
Request all keyboard input events.

This tries to ensure that the view receives keyboard events even when they
might normally be intercepted or consumed by the system or other handlers.
This is particularly useful for audio plugins when the host blocks key
events from reaching the plugin.

If you use this, you do not need to use puglGrabFocus.

Use the filter function to allow the exact events that you directly need,
allowing other handlers on the system to consume them if not.

Note that this will fail if the view is not mapped and so should not, for
example, be called immediately after puglShow().

@return #PUGL_SUCCESS if the focus was successfully grabbed, or an error.
*/
PUGL_API PuglStatus
puglSetWantsAllKeyboardEvents(PuglView* view,
bool wantsEvents,
PuglKeyboardEventFilter filterFunction);

/**
Request data from the general copy/paste clipboard.

Expand Down
16 changes: 16 additions & 0 deletions src/mac.m
Original file line number Diff line number Diff line change
Expand Up @@ -1451,6 +1451,22 @@ - (void)windowDidExitFullScreen:(NSNotification*)notification
[[impl->wrapperView window] firstResponder] == impl->wrapperView);
}

PuglStatus
puglSetWantsAllKeyboardEvents(PuglView* view,
bool wantsEvents,
PuglKeyboardEventFilter filterFunction)
{
(void)filterFunction;

if (wantsEvents) {
if (!puglHasFocus(view)) {
puglGrabFocus(view);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like trouble (across all implementations). I feel like it would be less sketchy if setting the flag was separate from grabbing focus, then the required setup done internally whenever focus is granted and released for whatever reason. This would work particularly nicely if PUGL_GREEDY_KEYS or some such was just a hint, and the filter function replaced with different semantics for the usual event handler (see other comments).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that could work and would be nicer.

}
}

return PUGL_SUCCESS;
}

static bool
styleIsMaximized(const PuglViewStyleFlags flags)
{
Expand Down
168 changes: 168 additions & 0 deletions src/win.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
#define PUGL_LOCAL_CLIENT_MSG (WM_USER + 52)
#define PUGL_USER_TIMER_MIN 9470

#define PUGL_WINDOW_MAGIC 0x5055474C // "PUGL" in ASCII
#define PUGL_WINDOW_MAGIC_OFFSET 0
#define PUGL_WINDOW_EXTRA_SIZE sizeof(LONG_PTR)

#ifdef __cplusplus
# define PUGL_INIT_STRUCT \
{ \
Expand All @@ -57,6 +61,9 @@ typedef HRESULT(WINAPI* PFN_GetScaleFactorForMonitor)(HMONITOR, DWORD*);
LRESULT CALLBACK
wndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);

static void
removeKeyboardHook(PuglView* view);

#ifdef UNICODE

typedef wchar_t ArgStringChar;
Expand Down Expand Up @@ -139,6 +146,7 @@ puglRegisterWindowClass(const char* name)
wc.cbSize = sizeof(wc);
wc.style = CS_OWNDC;
wc.lpfnWndProc = wndProc;
wc.cbWndExtra = PUGL_WINDOW_EXTRA_SIZE;
wc.hInstance = module;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
Expand Down Expand Up @@ -332,6 +340,7 @@ puglRealize(PuglView* view)
}

SetWindowLongPtr(impl->hwnd, GWLP_USERDATA, (LONG_PTR)view);
SetWindowLongPtr(impl->hwnd, PUGL_WINDOW_MAGIC_OFFSET, PUGL_WINDOW_MAGIC);

return puglDispatchSimpleEvent(view, PUGL_REALIZE);
}
Expand All @@ -350,6 +359,11 @@ puglUnrealize(PuglView* const view)
view->backend->destroy(view);
}

removeKeyboardHook(view);
if (view->impl->lastFocus) {
SetFocus(view->impl->lastFocus);
}

memset(&view->lastConfigure, 0, sizeof(PuglConfigureEvent));
ReleaseDC(impl->hwnd, impl->hdc);
impl->hdc = NULL;
Expand Down Expand Up @@ -921,6 +935,9 @@ handleMessage(PuglView* view, UINT message, WPARAM wParam, LPARAM lParam)
break;
case WM_KILLFOCUS:
event.type = PUGL_FOCUS_OUT;
if (view->impl->keyboardHook) {
removeKeyboardHook(view);
}
break;
case WM_SYSKEYDOWN:
initKeyEvent(&event.key, view, true, wParam, lParam);
Expand Down Expand Up @@ -1647,3 +1664,154 @@ puglWinLeave(PuglView* view, const PuglExposeEvent* expose)

return PUGL_SUCCESS;
}

// Returns true if the message was consumed
static bool
handleHookMessage(PuglView* view, const MSG* msg, int code, WPARAM wParam)
{
if (code != HC_ACTION || wParam != PM_REMOVE || !msg->hwnd) {
return false;
}

PuglEvent event = PUGL_INIT_STRUCT;

switch (msg->message) {
case WM_CHAR:
case WM_SYSCHAR:
initCharEvent(&event, view, msg->wParam, msg->lParam);
if (!view->impl->keyboardEventFilter ||
view->impl->keyboardEventFilter(view, &event)) {
puglDispatchEvent(view, &event);
return true;
}
return false;

case WM_KEYDOWN:
case WM_KEYUP:
case WM_SYSKEYDOWN:
case WM_SYSKEYUP: {
bool used = false;

// TODO: can we get the real CHAR event here rather than a made-up example
event.type = PUGL_TEXT;
event.text.character = 'a';
event.text.string[0] = 'a';
if (!view->impl->keyboardEventFilter ||
view->impl->keyboardEventFilter(view, &event)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how this works / does anything useful if it's calling the filter function on a made-up event with no relation to actual input? This applies to everything in this block since it's conditional on what the filter function returns.

@SamWindell SamWindell Apr 13, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The important differentiation here is character events vs key down/up events. If the plugin GUI has a text input boxed focused then we need to steal all character events from the host. Alternatively, the plugin GUI might have no need for text input but instead wants to detect key press CTRL + F, for example.

This is made messy because with Windows you first need to translate key events into character events using TranslateMessage. We should only call TranslateMessage when the plugin actually wants char events, otherwise it's not giving the host the opportunity to do this and things could behave strangely for them. That's as I understand it at least.

Here's what Floe does with it's keyboard event filter:

static void RequestAllKeyboardEvents(AppWindow& window, bool wants_focus) {
    puglSetWantsAllKeyboardEvents(
        window.view,
        wants_focus,
        [](PuglView* view, PuglEvent const* event) -> bool {
            auto& window = *(AppWindow*)puglGetHandle(view);
            switch (event->type) {
                case PUGL_TEXT: return window.last_result.wants.text_input;
                case PUGL_KEY_PRESS:
                case PUGL_KEY_RELEASE:
                    if (window.last_result.wants.text_input) return true;
                    if (auto const key_code = RemapKeyCode(event->key.key);
                        key_code && window.last_result.wants.keyboard_keys.Get(ToInt(*key_code)))
                        return true;
                    return false;
                default:
            }
            return false;
        });
}

// Adds a WM_CHAR message to the queue if the key produces text
TranslateMessage(msg);

// We check if text was produced (removing the events as we do the check)
MSG peeked = PUGL_INIT_STRUCT;
if (PeekMessage(&peeked, msg->hwnd, WM_CHAR, WM_DEADCHAR, PM_REMOVE) ||
PeekMessage(
&peeked, msg->hwnd, WM_SYSCHAR, WM_SYSDEADCHAR, PM_REMOVE)) {
used = true;
}
}

initKeyEvent(&event.key,
view,
msg->message == WM_KEYDOWN || msg->message == WM_SYSKEYDOWN,
msg->wParam,
msg->lParam);
if (!view->impl->keyboardEventFilter ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignoring the above for a moment, this pattern here is what I was initially thinking about with my initial knee-jerk dislike of adding a new filter function. The current scheme is:

  1. Call filter function to see if event is desired
  2. If so, dispatch

I think it would be better to instead do:

  1. Dispatch, and check the return value to see if the plugin consumed the event.

This would require a change in the rules for the event dispatch function so that e.g. PUGL_FAILURE is returned if the event isn't handled, but that's fine, because it would only affect code that opts in to "greedy" mode, and I think it's a good (and very common) principle for the event handler anyway.

I don't fully understand whether this is possible, though, because of the above funny business. What's the filter function you're using doing?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that would work, but as I understand it, on Windows, we shouldn't be calling TranslateMessage unless we know we want the event.

@SamWindell SamWindell Apr 13, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callback aspect could certainly still be avoided though. So long as the plugin has some way of communicating any mix of these requests:

  • I want to greedily steal all CHAR events
  • I want to greedily steal key events for X keys. (where X is one or more keys such as: F, for keyboard shortcut CTRL+F find, C, for CTRL+C copy, etc.)

view->impl->keyboardEventFilter(view, &event)) {
puglDispatchEvent(view, &event);
return true;
}

return used;
}
}

return false;
}

static LRESULT CALLBACK
keyboardHookProc(int code, WPARAM wParam, LPARAM lParam)
{
MSG* msg = (MSG*)lParam;

// Check if this window is actually a pugl window
if (GetClassLongPtr(msg->hwnd, GCL_CBWNDEXTRA) ==
(LONG_PTR)PUGL_WINDOW_EXTRA_SIZE &&
GetWindowLongPtr(msg->hwnd, PUGL_WINDOW_MAGIC_OFFSET) ==
PUGL_WINDOW_MAGIC) {
PuglView* view = (PuglView*)GetWindowLongPtr(msg->hwnd, GWLP_USERDATA);
if (view && view->impl && view->impl->wantsAllKeyboardEvents) {
if (handleHookMessage(view, msg, code, wParam)) {
// Scrub the message so no one else gets it
memset(msg, 0, sizeof(MSG));
msg->message = WM_USER;
return 0;
}
}
}

return CallNextHookEx(NULL, code, wParam, lParam);
}

static void
removeKeyboardHook(PuglView* view)
{
if (view->impl->keyboardHook) {
UnhookWindowsHookEx(view->impl->keyboardHook);
view->impl->keyboardHook = NULL;
}
}

static PuglStatus
installKeyboardHook(PuglView* view)
{
if (view->impl->keyboardHook) {
return PUGL_SUCCESS;
}

HMODULE module = NULL;
if (!GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCTSTR)keyboardHookProc,
&module)) {
module = GetModuleHandle(NULL);
}

if (!module) {
return PUGL_FAILURE;
}

view->impl->keyboardHook = SetWindowsHookEx(
WH_GETMESSAGE, keyboardHookProc, module, GetCurrentThreadId());

return view->impl->keyboardHook ? PUGL_SUCCESS : PUGL_FAILURE;
}

PuglStatus
puglSetWantsAllKeyboardEvents(PuglView* view,
bool wantsEvents,
PuglKeyboardEventFilter filterFunction)
{
if (!view || !view->impl) {
return PUGL_BAD_PARAMETER;
}

view->impl->wantsAllKeyboardEvents = wantsEvents;
view->impl->keyboardEventFilter = filterFunction;

if (wantsEvents) {
// Set focus and install hook
HWND hwnd = view->impl->hwnd;
if (GetFocus() != hwnd) {
view->impl->lastFocus = SetFocus(hwnd);
}
return installKeyboardHook(view);
} else {
// Restore previous focus and remove hook
if (view->impl->lastFocus) {
SetFocus(view->impl->lastFocus);
view->impl->lastFocus = NULL;
}
removeKeyboardHook(view);
return PUGL_SUCCESS;
}
}
36 changes: 20 additions & 16 deletions src/win.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,26 @@ struct PuglWorldInternalsImpl {
};

struct PuglInternalsImpl {
PuglWinPFD pfd;
int pfId;
HWND hwnd;
HCURSOR cursor;
HDC hdc;
WINDOWPLACEMENT oldPlacement;
PAINTSTRUCT paint;
PuglBlob clipboard;
PuglSurface* surface;
double scaleFactor;
bool mapped;
bool flashing;
bool mouseTracked;
bool minimized;
bool maximized;
bool fullscreen;
PuglWinPFD pfd;
int pfId;
HWND hwnd;
HCURSOR cursor;
HDC hdc;
WINDOWPLACEMENT oldPlacement;
PAINTSTRUCT paint;
PuglBlob clipboard;
PuglSurface* surface;
double scaleFactor;
bool mapped;
bool flashing;
bool mouseTracked;
bool minimized;
bool maximized;
bool fullscreen;
HWND lastFocus;
HHOOK keyboardHook;
bool wantsAllKeyboardEvents;
PuglKeyboardEventFilter keyboardEventFilter;
};

PUGL_API PuglWinPFD
Expand Down
16 changes: 16 additions & 0 deletions src/x11.c
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,22 @@ puglHasFocus(const PuglView* const view)
return focusedWindow == view->impl->win;
}

PuglStatus
puglSetWantsAllKeyboardEvents(PuglView* view,
bool wantsEvents,
PuglKeyboardEventFilter filterFunction)
{
(void)filterFunction;

if (wantsEvents) {
if (!puglHasFocus(view)) {
puglGrabFocus(view);
}
}

return PUGL_SUCCESS;
}

PuglStatus
puglStartTimer(PuglView* const view, const uintptr_t id, const double timeout)
{
Expand Down