|
wx_BGI_Graphics
Classic BGI-compatible graphics API with modern OpenGL extension API
|
This document explains how wx_BGI_Graphics captures, translates, queues, and exposes keyboard and mouse input to the programmer. It covers the low-level GLFW backend APIs, the data structures involved, exactly what the library's built-in callbacks do before any user hook fires, the user hook (callback chaining) system, the threading model, and a full code map of every relevant function.
Input handling in wx_BGI_Graphics is a synchronous, single-threaded, callback-driven pipeline. The library supports two backends:
WxBgiCanvas translates wx events into the same internal BGI state updates. wxbgi_poll_events() is a no-op in this mode.WXBGI_ENABLE_WX=OFF): The programmer drives the loop by calling wxbgi_poll_events() which calls glfwPollEvents().In both modes, keyboard events are buffered in an in-memory queue; mouse position is tracked in plain integer fields; mouse button clicks directly trigger the object pick/selection system. After each built-in callback completes, an optional user hook is called if one has been registered.
wx-mode pipeline:
GLFW-mode pipeline:
The library relies exclusively on GLFW 3.4 for window management and raw input events. No platform-specific input APIs (Win32 GetAsyncKeyState, X11 XNextEvent, Cocoa NSEvent) are used directly – GLFW abstracts them.
| GLFW API | Purpose |
|---|---|
glfwSetKeyCallback(window, cb) | Registers the keyboard key callback |
glfwSetCharCallback(window, cb) | Registers the Unicode character callback |
glfwSetCursorPosCallback(window, cb) | Registers the mouse position callback |
glfwSetMouseButtonCallback(window, cb) | Registers the mouse button callback |
glfwPollEvents() | Processes all pending OS events; fires registered callbacks synchronously |
glfwWindowShouldClose(window) | Checks whether the user has closed the window |
Callback signatures (as required by GLFW):
All four callbacks are registered in initializeWindow() immediately after glewInit():
Important: GLFW supports only one registered callback per event type per window. The library owns all four callbacks. Users must not call
glfwSet*Callback()directly – doing so would silently destroy the key queue, keyDown state, mouse tracking, and selection system. Use thewxbgi_set_*_hook()API instead (see User Input Hooks).
Two GLFW callbacks cooperate to cover the full range of keyboard input.
Fires on every key press, repeat, and release.
Step-by-step actions performed by the library:
keyDown[] update (always, for all actions including release):** Sets gState.keyDown[key] = (action != GLFW_RELEASE) ? 1 : 0. This keeps the instantaneous raw-key state table current for wxbgi_is_key_down().gState.keyQueue:{0, scancode} sequence (see DOS-Style Extended Key Codes)charCallback (avoids double-queuing)userKeyHook invocation (after all library logic, all actions):** If gState.userKeyHook != nullptr, it is called with (key, scancode, action, mods). By this point keyDown[key] reflects the new state and any key code has already been enqueued.State guaranteed to be current when WxbgiKeyHook fires:
gState.keyDown[key] – already updated to the new press/release stategState.keyQueue – already has the translated code pushed (for press/repeat of special keys)Fires only for printable Unicode input (letter, digit, symbol with any modifier). GLFW fires this in addition to keyCallback for printable keys, giving the OS-composed character (correct layout, Shift, dead keys, IME).
Step-by-step actions performed by the library:
keyCallback.gState.keyQueue.userCharHook invocation:** If gState.userCharHook != nullptr, called with (codepoint). The code is already in keyQueue by this point.State guaranteed to be current when WxbgiCharHook fires:
gState.keyQueue – already contains the just-pushed codepointWhy two callbacks? GLFW guarantees that charCallback produces the correct OS-composed character (honouring keyboard layout, Shift, dead keys). Using it for printable characters and keyCallback for control/special keys avoids duplication and correctly handles international layouts.
queueExtendedKey() produces the classic two-byte sequence {0, scancode} that DOS programs expect for non-printable keys.
Classic Borland BGI programs read the keyboard with getch(), which returns 0 for extended keys and then returns the scancode on the next call. This library emulates that behaviour exactly by pushing two integers for each special key.
| Key | GLFW constant | Scancode | Queue sequence |
|---|---|---|---|
| Up arrow | GLFW_KEY_UP | 72 | 0, 72 |
| Down arrow | GLFW_KEY_DOWN | 80 | 0, 80 |
| Left arrow | GLFW_KEY_LEFT | 75 | 0, 75 |
| Right arrow | GLFW_KEY_RIGHT | 77 | 0, 77 |
| Home | GLFW_KEY_HOME | 71 | 0, 71 |
| End | GLFW_KEY_END | 79 | 0, 79 |
| Page Up | GLFW_KEY_PAGE_UP | 73 | 0, 73 |
| Page Down | GLFW_KEY_PAGE_DOWN | 81 | 0, 81 |
| Insert | GLFW_KEY_INSERT | 82 | 0, 82 |
| Delete | GLFW_KEY_DELETE | 83 | 0, 83 |
| F1 | GLFW_KEY_F1 | 59 | 0, 59 |
| F2 | GLFW_KEY_F2 | 60 | 0, 60 |
| F3 | GLFW_KEY_F3 | 61 | 0, 61 |
| F4 | GLFW_KEY_F4 | 62 | 0, 62 |
| F5 | GLFW_KEY_F5 | 63 | 0, 63 |
| F6 | GLFW_KEY_F6 | 64 | 0, 64 |
| F7 | GLFW_KEY_F7 | 65 | 0, 65 |
| F8 | GLFW_KEY_F8 | 66 | 0, 66 |
| F9 | GLFW_KEY_F9 | 67 | 0, 67 |
| F10 | GLFW_KEY_F10 | 68 | 0, 68 |
| F11 | GLFW_KEY_F11 | 133 | 0, 133 |
| F12 | GLFW_KEY_F12 | 134 | 0, 134 |
Single-byte control characters (no prefix):
| Key | Code |
|---|---|
| Escape | 27 |
| Enter / KP Enter | 13 |
| Tab | 9 |
| Backspace | 8 |
Defined in src/bgi_types.h, struct BgiState:
All public keyboard functions are declared in src/wx_bgi_ext.h and implemented in src/bgi_modern_api.cpp. Each acquires gMutex via std::lock_guard.
Returns 1 if at least one key code is waiting in keyQueue, 0 if empty. Does not consume the queued code. Equivalent to the DOS kbhit() function.
Usage pattern:
Dequeues and returns the next translated key code (0-255), or -1 if the queue is empty. For extended keys, the first call returns 0 and the next call returns the scancode.
Usage pattern:
Queries the instantaneous press state of a key by its GLFW constant. Returns 1 if currently held, 0 if released, -1 if key is out of range. Bypasses the queue – useful for detecting held modifier or navigation keys without consuming queue entries.
Usage pattern:
When compiled with WXBGI_ENABLE_TEST_SEAMS, three additional functions are available for CI keyboard injection:
| Function | Purpose |
|---|---|
wxbgi_test_clear_key_queue() | Empties keyQueue |
wxbgi_test_inject_key_code(int) | Pushes one code (0-255) into keyQueue |
wxbgi_test_inject_extended_scan(int) | Pushes {0, scanCode} into keyQueue |
These functions acquire gMutex and directly manipulate gState.keyQueue, simulating what the GLFW callbacks would do during real key presses.
Fires on every mouse movement inside the window.
Step-by-step actions performed by the library:
double xpos, ypos to int and writes into gState.mouseX and gState.mouseY. Origin is top-left (0,0); matches BGI pixel space.gState.mouseMoved = true. Cleared by wxbgi_mouse_moved().userCursorPosHook invocation:** If non-null, called with (int x, int y) – the same values already written to gState.mouseX/Y.State guaranteed to be current when WxbgiCursorPosHook fires:
gState.mouseX, gState.mouseY – already updated to the new positiongState.mouseMoved – already set to trueFires on any mouse button press or release.
Step-by-step actions performed by the library:
button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS:mods & GLFW_MOD_CONTROL)overlayPerformPick(gState.mouseX, gState.mouseY, ctrl) which:selCursorOverlay.enabledselectionPickRadiusPx pixelsgState.selectedObjectIds (single-click: clear+add or deselect; CTRL+click: toggle)userMouseButtonHook invocation (all buttons, all actions):** If non-null, called with (button, action, mods).State guaranteed to be current when WxbgiMouseButtonHook fires:
gState.mouseX, gState.mouseY – already current (from last cursorPosCallback)gState.selectedObjectIds – already updated by pick logic (for left-press events)Scroll wheel:
glfwSetScrollCallbackis not registered. Scroll events are not captured in the current implementation.
GLFW delivers mouse positions in window-pixel space with the origin at the top-left corner:
These coordinates match the BGI pixel coordinate system directly – no transformation is applied. gState.mouseX/Y are ready to use for BGI-pixel-space hit tests.
Defined in src/bgi_types.h, struct BgiState:
Declared in src/wx_bgi_ext.h, implemented in src/bgi_modern_api.cpp.
Writes the current cursor position into *x and *y. Either pointer may be NULL. Acquires gMutex.
Returns 1 if the cursor has moved since the last call to this function, 0 otherwise. Atomically reads and clears gState.mouseMoved. Acquires gMutex.
Typical usage in an animation loop:
Left mouse button clicks drive the DDS object selection system. This pipeline runs inside mouseButtonCallback before the user's WxbgiMouseButtonHook fires:
overlayPerformPick() (src/bgi_overlay.cpp) handles all DDS geometry types: 2D world lines/circles/rectangles, 3D solids, and 3D surfaces. Depth ordering uses the camera view matrix to project candidate centroids and sort by Z-depth.
Flash feedback for selected objects is driven by gState.solidColorOverride – see VisualAids.md for details.
The event pump causes GLFW to deliver accumulated OS events to the registered callbacks. It must be called on the main thread.
gMutex first.glfwPollEvents() while holding the lock.gMutex already held.glfwPollEvents() is also called inside wxbgi_begin_advanced_frame(), which is used in custom render loops that want tight frame-rate control with automatic event processing.
keyQueue stays empty – wxbgi_read_key() always returns -1.mouseX/mouseY never update – wxbgi_get_mouse_pos() returns stale values.wxbgi_should_close() never returns 1.gMutex guards all access to bgi::gState, including all keyboard, mouse, and hook fields.
| Location | Acquires gMutex? | Reason |
|---|---|---|
wxbgi_poll_events() | YES – std::lock_guard | Public API entry point |
wxbgi_key_pressed() | YES – std::lock_guard | Public API |
wxbgi_read_key() | YES – std::lock_guard | Public API |
wxbgi_is_key_down() | YES – std::lock_guard | Public API |
wxbgi_get_mouse_pos() | YES – std::lock_guard | Public API |
wxbgi_mouse_moved() | YES – std::lock_guard | Public API |
wxbgi_set_key_hook() | YES – std::lock_guard | Public API |
wxbgi_set_char_hook() | YES – std::lock_guard | Public API |
wxbgi_set_cursor_pos_hook() | YES – std::lock_guard | Public API |
wxbgi_set_mouse_button_hook() | YES – std::lock_guard | Public API |
keyCallback() | NO | Called while lock held by glfwPollEvents() caller |
charCallback() | NO | Called while lock held by glfwPollEvents() caller |
cursorPosCallback() | NO | Called while lock held by glfwPollEvents() caller |
mouseButtonCallback() | NO | Called while lock held by glfwPollEvents() caller |
overlayPerformPick() | NO | Called from mouseButtonCallback inside locked context |
User hooks (userKeyHook etc.) | NO | Called from within GLFW callbacks – mutex already held |
std::mutex is non-recursive in C++. If any callback or user hook attempted to acquire gMutex again on the same thread (already locked by wxbgi_poll_events()), the behaviour is undefined – on MSVC debug builds it calls abort() immediately. This rule is documented in the source at src/bgi_api.cpp.
The library provides an optional hook API that lets programs in any language receive keyboard and mouse events directly, alongside the library's own internal processing. This is the correct way to extend input handling – never call glfwSet*Callback() directly.
The library retains full ownership of all 4 GLFW callbacks. Hooks are additive: the library's own logic always runs first, then the user hook is called if registered.
Pass NULL to any registration function to remove a previously installed hook.
This table summarises exactly what the library has done by the time your hook fires. Use it when implementing hooks to know what state is already up-to-date.
| Callback / Hook | Trigger | Library actions completed before hook fires | State ready in hook |
|---|---|---|---|
keyCallback / WxbgiKeyHook | Any key press, repeat, or release | keyDown[key] updated to new state; for press/repeat of special/control keys: translated code(s) pushed to keyQueue | gState.keyDown[key] is current; gState.keyQueue has any new codes |
charCallback / WxbgiCharHook | Printable character typed | Codepoint (1-255) pushed to keyQueue; out-of-range and Tab/Enter/Esc dropped | gState.keyQueue front is the just-pushed codepoint |
cursorPosCallback / WxbgiCursorPosHook | Mouse cursor moved | mouseX and mouseY set to new position (int pixels); mouseMoved = true | gState.mouseX, gState.mouseY are current new position |
mouseButtonCallback / WxbgiMouseButtonHook | Any mouse button pressed or released | Left press only: overlayPerformPick() ran and selectedObjectIds updated (add/toggle/deselect). Right/middle/release: no library action | gState.selectedObjectIds reflects latest selection; gState.mouseX/Y are current |
Additional notes:
keyCallback fires for all keys on press, repeat, and release. charCallback fires only for printable keys on press (and repeat if OS sends char repeats). For a plain letter like 'A', both keyCallback (GLFW key code 65) AND charCallback (codepoint 65 or 97 depending on shift) will fire – the char codepoint is what goes into keyQueue.CRITICAL: User hooks fire from within a GLFW callback, which executes synchronously inside
glfwPollEvents()whilegMutexis held. Hooks must not call anywxbgi_*function – doing so deadlocks on the non-recursivestd::mutex(and callsabort()in MSVC debug builds).Safe from a hook: update your own variables, set flags, call non-wxbgi code, enqueue to your own data structures, call OS/platform APIs that don't re-enter the library.
Unsafe from a hook: any
wxbgi_*call, includingwxbgi_get_mouse_pos(),wxbgi_read_key(),wxbgi_poll_events(), etc.
Read mouse/keyboard state after wxbgi_poll_events() returns using the normal public API:
The following typedefs are defined in src/bgi_types.h at global scope, available to all languages without requiring GLFW headers:
BGI_CALL is __cdecl on MSVC Windows and empty (default C calling convention) elsewhere.
Defined in src/wx_bgi_ext.h; mirror GLFW values exactly:
| Constant | Value | Meaning |
|---|---|---|
WXBGI_KEY_PRESS | 1 | Key or button pressed |
WXBGI_KEY_RELEASE | 0 | Key or button released |
WXBGI_KEY_REPEAT | 2 | Key held (auto-repeat) |
WXBGI_MOUSE_LEFT | 0 | Left mouse button |
WXBGI_MOUSE_RIGHT | 1 | Right mouse button |
WXBGI_MOUSE_MIDDLE | 2 | Middle mouse button |
WXBGI_MOD_SHIFT | 0x0001 | Shift modifier held |
WXBGI_MOD_CTRL | 0x0002 | Control modifier held |
WXBGI_MOD_ALT | 0x0004 | Alt modifier held |
All four functions are declared in src/wx_bgi_ext.h and implemented in src/bgi_modern_api.cpp. Each acquires gMutex briefly to store the pointer, then returns immediately.
| Function | Hook fires when | Parameters |
|---|---|---|
wxbgi_set_key_hook | Any key press, repeat, or release | key (GLFW key code), scancode (OS scancode), action (WXBGI_KEY_*), mods (bitfield) |
wxbgi_set_char_hook | Printable character typed (1-255) | codepoint (unsigned int, OS-composed) |
wxbgi_set_cursor_pos_hook | Mouse cursor moves | x, y (window pixels, top-left origin) |
wxbgi_set_mouse_button_hook | Any mouse button press or release | button (WXBGI_MOUSE_*), action (WXBGI_KEY_PRESS/RELEASE), mods |
The library captures scroll wheel input via GLFW's glfwSetScrollCallback and provides both an accumulation API and a hook for real-time scroll reaction.
scrollCallback(window, xoffset, yoffset) during glfwPollEvents().WXBGI_DEFAULT_SCROLL_ACCUM is active, xoffset/yoffset are added to gState.scrollDeltaX / gState.scrollDeltaY.userScrollHook (if any) fires with the raw per-event deltas.Call wxbgi_get_scroll_delta() from your main loop to atomically read and clear the accumulated totals:
| Function | Description |
|---|---|
wxbgi_set_scroll_hook(cb) | Install (or remove with NULL) the scroll hook. |
wxbgi_get_scroll_delta(dx, dy) | Read + clear accumulated scroll deltas. |
By default all four built-in callback behaviors are active. You can disable any combination selectively using wxbgi_set_input_defaults().
| Scenario | Bypass flag |
|---|---|
| Rolling your own key buffer | WXBGI_DEFAULT_KEY_QUEUE |
| Implementing your own mouse tracker | WXBGI_DEFAULT_CURSOR_TRACK |
| Replacing the auto-pick on left-click | WXBGI_DEFAULT_MOUSE_PICK |
| Handling scroll yourself (camera zoom etc.) | WXBGI_DEFAULT_SCROLL_ACCUM |
| Constant | Value | Controls |
|---|---|---|
WXBGI_DEFAULT_KEY_QUEUE | 0x01 | keyCallback queues key codes; charCallback queues chars |
WXBGI_DEFAULT_CURSOR_TRACK | 0x02 | cursorPosCallback updates mouseX/mouseY/mouseMoved |
WXBGI_DEFAULT_MOUSE_PICK | 0x04 | mouseButtonCallback calls overlayPerformPick() on left-click |
WXBGI_DEFAULT_SCROLL_ACCUM | 0x08 | scrollCallback accumulates scrollDeltaX/Y |
WXBGI_DEFAULT_ALL | 0x0F | All defaults active (initial state) |
WXBGI_DEFAULT_NONE | 0x00 | All defaults disabled |
Note:
keyDown[](raw hardware state) is always updated regardless of bypass flags. User hooks always fire regardless of bypass flags.
All wxbgi_* functions acquire gMutex internally. Because user hooks fire while the mutex is held (inside glfwPollEvents()), calling any ordinary wxbgi_* function from a hook causes a deadlock.
The wxbgi_hk_* family provides safe alternatives that skip mutex acquisition. Call them only from within a registered hook callback.
Outside a hook, calling wxbgi_hk_* directly is technically a data race in multi-threaded code. In single-threaded programs (the common case for BGI applications) it is safe.
| Function | Description |
|---|---|
wxbgi_hk_get_mouse_x() | Current cursor X (pixels). |
wxbgi_hk_get_mouse_y() | Current cursor Y (pixels). |
wxbgi_hk_dds_get_selected_count() | Number of currently selected DDS objects. |
wxbgi_hk_dds_get_selected_id(i, buf, maxLen) | Copy ID of the i-th selected object into buf. Returns string length or -1. |
wxbgi_hk_dds_is_selected(id) | Returns 1 if the object with that ID is selected. |
wxbgi_hk_dds_select(id) | Add an object to the selection. |
wxbgi_hk_dds_deselect(id) | Remove an object from the selection. |
wxbgi_hk_dds_deselect_all() | Clear the entire selection. |
wxbgi_hk_dds_pick_at(x, y, ctrl) | Run the spatial pick algorithm at (x,y). ctrl!=0 = toggle. Returns new count. |
| Program | Description |
|---|---|
examples/cpp/test_input_hooks.cpp | Automated: registers/deregisters all hooks; simulates key/char/cursor/mouse via test seams; 20+ assertions. |
examples/cpp/test_input_bypass.cpp | Automated: scroll hook, WXBGI_DEFAULT_* bypass flags, wxbgi_hk_* DDS functions. |
examples/cpp/wxbgi_input_hooks.cpp | Interactive demo: 4 status panels, arrow-key marker, char text area, mouse crosshair, click stamps. |
| Program | Description |
|---|---|
examples/demoFreePascal/test_input_hooks.pas | Pascal mirror of test_input_hooks.cpp. |
examples/demoFreePascal/test_input_bypass.pas | Pascal mirror of test_input_bypass.cpp. |
examples/demoFreePascal/demo_input_hooks.pas | Pascal interactive hook demo. |
The test seam functions simulate the exact same callback->state->hook pipeline as real GLFW events:
| Seam function | Simulates |
|---|---|
wxbgi_test_simulate_key(key, scan, action, mods) | keyCallback |
wxbgi_test_simulate_char(codepoint) | charCallback |
wxbgi_test_simulate_cursor(x, y) | cursorPosCallback (respects WXBGI_DEFAULT_CURSOR_TRACK) |
wxbgi_test_simulate_mouse_button(btn, action, mods) | mouseButtonCallback (omits pick; needs framebuffer) |
wxbgi_test_simulate_scroll(xoffset, yoffset) | scrollCallback (respects WXBGI_DEFAULT_SCROLL_ACCUM) |
See also:
src/wx_bgi_ext.h** – complete public declarations with Doxygen doc-commentsexamples/cpp/wxbgi_keyboard_queue.cpp** – live example of the keyboard queue APIFully automated test for all four user hook types. Registered as a CTest test.
Phase 1 (always): verifies that wxbgi_set_key_hook / wxbgi_set_char_hook / wxbgi_set_cursor_pos_hook / wxbgi_set_mouse_button_hook and their NULL-deregistration counterparts complete without crashing.
Phase 2 (requires WXBGI_ENABLE_TEST_SEAMS): uses the simulation seams to drive the full callback -> hook pipeline:
wxbgi_is_key_down is updated.{0, 72} sequence appears in keyQueue.); verifies hook fires and char is inkeyQueue.Verifies ESC / out-of-range codepoints are silently dropped (hook NOT called, queue NOT updated).Simulates cursor moves; verifies hook fires andwxbgi_get_mouse_pos/wxbgi_mouse_movedreflect the new position.Simulates left, right, and release mouse button events with modifier keys.Verifies that setting a hook tonullptr` stops it from firing.Phase 3 (always): draws summary primitives (circle at last simulated cursor position, colored rectangles sized by fire counts) to confirm the BGI drawing pipeline is unaffected.
Build this test with WXBGI_ENABLE_TEST_SEAMS=ON for the full simulation phase:
Pascal mirror of the C++ test. Uses {$IFDEF WXBGI_ENABLE_TEST_SEAMS} to gate the simulation phase. When CMake is configured with -DWXBGI_ENABLE_TEST_SEAMS=ON, the FPC compiler receives -dWXBGI_ENABLE_TEST_SEAMS automatically. Two CTest tests are registered: test_input_hooks_pascal_build (compiles the Pascal program) and test_input_hooks_pascal_run (runs it).
Hook typedefs in Pascal (cdecl calling convention):
Interactive demo showing all four hook types firing in real time. Not a CTest test – run it manually from the build output directory.
| Input | Effect |
|---|---|
| Arrow keys | Move red crosshair marker |
C key | Cycle stamp colour |
| Typed printable chars | Appear in text bar (Backspace removes last) |
| Mouse move | Yellow crosshair follows cursor |
| Left click | Stamp filled circle at cursor |
| Right click | Clear all stamps |
| Escape | Exit |
Four status panels across the top show each hook's last event and fire count in real time.
Pascal interactive demo with identical behaviour to the C++ demo. Build with:
Then run pascal_input_hooks/demo_input_hooks[.exe] from the build directory.
The following functions are exported only when WXBGI_ENABLE_TEST_SEAMS=1. They faithfully replicate the internal callback pipeline so automated tests can exercise hooks without real OS input:
| Function | Replicates | State updated before hook fires |
|---|---|---|
wxbgi_test_simulate_key(key, scan, action, mods) | keyCallback | keyDown[key]; keyQueue (special keys) |
wxbgi_test_simulate_char(codepoint) | charCallback | keyQueue (filtered chars only) |
wxbgi_test_simulate_cursor(x, y) | cursorPosCallback | mouseX, mouseY, mouseMoved |
wxbgi_test_simulate_mouse_button(btn, action, mods) | mouseButtonCallback (hook path only) | *(no state change; overlayPerformPick omitted)* |
Note: wxbgi_test_simulate_char applies the same filter as charCallback – codepoints <= 0,
255, or equal to 9 / 13 / 27 are dropped entirely (hook not called, queue not updated).
This matches the real callback behaviour exactly.