/* ** Win32 Typing Test Program ** v1.1.0 - Jan 19th 2024 ** by Allen Webster allenw@mr4th.com ** ** public domain example program ** NO WARRANTY IMPLIED; USE AT YOUR OWN RISK ** ** ** Tested on: ** Windows 10 (English, Latvian, Emojis) ** ** This is an example and a tool for gathering information about ** Win32 text input APIs. ** ** You may use it as a starting point for your own Win32 text input ** implementation. ** ** Or you may contribute to the project by: ** Trying typing in the test program and report any misbehavior you see. ** Supplying references to relevant documentation/examples that perform ** this task in a better way. ** */ #include typedef LONG S32; typedef USHORT U16; typedef ULONG U32; //////////////////////////////// // App Functions static void AppInit(HWND wnd); static void AppRender(HWND wnd); static void AppTypeBackspace(void); static void AppTypeCodepoint(U32 cp); //////////////////////////////// // Window Proc static BOOL keep_running = 1; static WCHAR lo_surrogate = 0; static WCHAR hi_surrogate = 0; static LRESULT WindowProc(HWND wnd, UINT msg, WPARAM wparam, LPARAM lparam){ LRESULT result = 0; switch (msg){ case WM_CHAR: { // When a WM_CHAR generally represents text input that // should be inserted, but there are a few twists. // // Sometimes WM_CHAR is triggered by controls that are // not text input, like Ctrl + Letter. To filter out // these cases we ignore values less than ' ' (0x20) // and the value 0x7F. See WM_KEYDOWN for a little // more on this issue. // // When the user types a character that does not fit // in a single U16, a pair of WM_CHAR messages are sent. // Each WM_CHAR message in the pair contains a // "surrogate" (defined by UTF16) one "high" surrogate // followed by a "low" surrogate. A pair of surrogates // translates into a single typed codepoint. // // (Actually the docs don't clearly say one order // or the other - I just support both) U32 c = (wparam & 0xFFFF); if (c >= ' ' && c != 0x7F){ if (IS_LOW_SURROGATE(c)){ lo_surrogate = c; } else if (IS_HIGH_SURROGATE(c)){ hi_surrogate = c; } else{ AppTypeCodepoint(c); lo_surrogate = 0; hi_surrogate = 0; } if (hi_surrogate != 0 && lo_surrogate != 0){ if (IS_SURROGATE_PAIR(hi_surrogate, lo_surrogate)){ U32 cp = (0x10000 + ((hi_surrogate & 0x3FF) << 10) + (lo_surrogate & 0x3FF) ); AppTypeCodepoint(cp); } hi_surrogate = 0; lo_surrogate = 0; } } }break; case WM_KEYDOWN: { // While it is possible to see newlines/carriage returns // and tabs through the WM_CHAR message, it is also possible // to get erroneous newlines and tabs in WM_CHAR. // // "Erroneous" WM_CHAR messages are generated when a user // holds down "Ctrl" and presses any alphabetic character. // The erroneous values generated this way are always less // than 0x20 (0x20 is the ascii for the space character ' '). // // Thus it's possible to get simple behavior by ignoring // all the WM_CHAR messages that have character values less // than 0x20 and then directly handle these two relevant // cases here. if (wparam == VK_RETURN){ AppTypeCodepoint('\n'); } if (wparam == VK_TAB){ AppTypeCodepoint('\t'); } if (wparam == VK_BACK){ AppTypeBackspace(); } }break; case WM_SYSCOMMAND: { // Not directly relevant to the main goal, but if you don't // want your Alt+Key combos to play a bell sound you have to // disable default handling of this 'menu' path BOOL is_menu = (wparam == SC_KEYMENU && (lparam >> 16) <= 0); if (!is_menu){ result = DefWindowProcW(wnd, msg, wparam, lparam); } }break; case WM_PAINT: { PAINTSTRUCT ps; BeginPaint(wnd, &ps); AppRender(wnd); EndPaint(wnd, &ps); }break; case WM_CLOSE: { keep_running = 0; }break; default: { result = DefWindowProcW(wnd, msg, wparam, lparam); }break; } return(result); } //////////////////////////////// // Main static BOOL composition_enabled = 0; int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd){ WNDCLASSW window_class = {0}; window_class.style = CS_HREDRAW|CS_VREDRAW; window_class.lpfnWndProc = WindowProc; window_class.hInstance = hInstance; window_class.hCursor = LoadCursorA(0, IDC_ARROW); window_class.lpszClassName = L"WindowClass"; ATOM atom = RegisterClassW(&window_class); if (atom == 0){ MessageBoxW(0, (WCHAR*)L"RegisterClassW failed\n", (WCHAR*)L"Fatal", MB_OK); ExitProcess(1); } DWORD style = WS_OVERLAPPEDWINDOW; HWND wnd = CreateWindowW(L"WindowClass", L"Type Test", style, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, hInstance, 0); // init AppInit(wnd); // main loop ShowWindow(wnd, SW_SHOW); keep_running = 1; for (;keep_running;){ // Handle Message Queue BOOL wait = 1; MSG msg; for (;(wait?GetMessageW(&msg, 0, 0, 0): PeekMessageW(&msg, 0, 0, 0, PM_REMOVE));){ wait = 0; TranslateMessage(&msg); DispatchMessageW(&msg); } // render AppRender(wnd); // sleep Sleep(33); } } //////////////////////////////// // App Implementation static HFONT font = 0; #define TEXT_CAP (1 << 14) static U16 text_memory[TEXT_CAP]; static U32 text_len = 0; static void AppInsertCodepoint(U32 cp){ // convert codepoint to UTF16 U16 encode_buf[2]; U32 encode_len = 0; if (cp < 0x10000){ encode_buf[0] = cp; encode_len = 1; } else{ U32 cpj = cp - 0x10000; encode_buf[0] = (cpj >> 10) + 0xD800; encode_buf[1] = (cpj & 0x3FF) + 0xDC00; encode_len = 2; } // insert into string if (text_len + encode_len < TEXT_CAP){ for (U32 i = 0; i < encode_len; i += 1){ text_memory[text_len] = encode_buf[i]; text_len += 1; } } } static void AppTypeBackspace(void){ text_len = 0; } static void AppTypeCodepoint(U32 cp){ if (cp == '\n'){ AppInsertCodepoint('\\'); AppInsertCodepoint('n'); } else if (cp == '\t'){ AppInsertCodepoint('\\'); AppInsertCodepoint('t'); } else{ AppInsertCodepoint(cp); } } static void AppInit(HWND wnd){ LOGFONT logfont = {0}; logfont.lfHeight = 24; memcpy(logfont.lfFaceName, "Arial", sizeof("Arial")); font = CreateFontIndirectA(&logfont); } static void AppRender(HWND wnd){ // window dimensions RECT wnd_rect = {0}; GetClientRect(wnd, &wnd_rect); // render begin HDC wnd_dc = GetDC(wnd); HDC dc = CreateCompatibleDC(wnd_dc); HBITMAP bitmap = CreateCompatibleBitmap(wnd_dc, wnd_rect.right, wnd_rect.bottom); SelectObject(dc, bitmap); // render body { SelectObject(dc, GetStockObject(DC_PEN)); SelectObject(dc, GetStockObject(DC_BRUSH)); SetBkMode(dc, TRANSPARENT); SelectObject(dc, font); // background { SetDCPenColor(dc, RGB(255, 255, 255)); SetDCBrushColor(dc, RGB(255, 255, 255)); Rectangle(dc, wnd_rect.left, wnd_rect.top, wnd_rect.right, wnd_rect.bottom); } // string { SIZE dim = {0}; GetTextExtentPoint32W(dc, (WCHAR*)text_memory, text_len, &dim); S32 cx = (wnd_rect.right - wnd_rect.left)/2; S32 cy = (wnd_rect.bottom - wnd_rect.top)/2; S32 x = cx - dim.cx/2; S32 y = cy - dim.cy/2; TextOutW(dc, x, y, (WCHAR*)text_memory, text_len); } } // render end BitBlt(wnd_dc, 0, 0, wnd_rect.right, wnd_rect.bottom, dc, 0, 0, SRCCOPY); DeleteObject(bitmap); DeleteDC(dc); ReleaseDC(wnd, wnd_dc); } //$ graphical //