commit d73eba0790cccb72e240004425e0cb3cce9d050c Author: Allen Webster Date: Fri Jan 19 18:57:30 2024 -0800 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf8ef28 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.* +build/* \ No newline at end of file diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..0b4b8cb --- /dev/null +++ b/build.bat @@ -0,0 +1,11 @@ +@echo off + +set libs=User32.lib Dwmapi.lib Gdi32.lib + +set opts=-FC -GR- -EHa- -nologo -Zi +set code=%cd% + +if not exist build ( mkdir build ) +pushd build +cl %opts% %code%\win32_typing_test.c %libs% -Fetyping_test +popd diff --git a/project.4coder b/project.4coder new file mode 100644 index 0000000..5757724 --- /dev/null +++ b/project.4coder @@ -0,0 +1,31 @@ +version(2); +project_name = "Typing Test"; +patterns = { +"*.c", +"*.h", +"*.bat", +"*.4coder", +}; +blacklist_patterns = { +".*", +}; +load_paths_base = { + { ".", .relative = true, .recursive = true, }, +}; +load_paths = { { load_paths_base, .os = "win", }, }; + +commands = { + .build = + { .out = "*compilation*", .footer_panel = true, .save_dirty_files = true, + .win = "build.bat" }, + + .run = + { .out = "*run*", .footer_panel = false, .save_dirty_files = false, + .win = "build\\typing_test" }, +}; + + +fkey_command = { + .F1 = "build", + .F2 = "run", +}; diff --git a/win32_typing_test.c b/win32_typing_test.c new file mode 100644 index 0000000..043a214 --- /dev/null +++ b/win32_typing_test.c @@ -0,0 +1,328 @@ +/* +** A program for testing win32 text input +*/ + +#include + +// only for vsync in GDI +#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 LRESULT +WindowProc(HWND wnd, + UINT msg, + WPARAM wparam, + LPARAM lparam){ + LRESULT result = 0; + + switch (msg){ + case WM_PAINT: + { + PAINTSTRUCT ps; + BeginPaint(wnd, &ps); + AppRender(wnd); + EndPaint(wnd, &ps); + }break; + + case WM_CHAR: + { + // When a user uses IME, their input is handled by + // WM_IME_CHAR messages, but additional WM_CHAR messages + // get generated in the process. + // + // I'm not entirely sure what the best thing is to do with + // them - but they appear to always be messages I want to ignore. + // + // They also appear to be the only case where a WM_CHAR + // message has a scan code of zero. + // + // Therefore my best solution so far is to filter out WM_CHAR + // messages that have a zero scan code. + + U32 scan_code = ((lparam >> 16) & 0xFF); + if (scan_code != 0){ + U32 cp16 = (wparam & 0xFFFF); + if (cp16 >= ' ' && cp16 != 0x7F){ + AppTypeCodepoint(cp16); + } + } + }break; + + case WM_IME_CHAR: + { + U32 cp16 = (wparam & 0xFFFF); + if (cp16 >= ' ' && cp16 != 0x7F){ + AppTypeCodepoint(cp16); + } + }break; + + case WM_UNICHAR: + { + U32 cp32 = wparam; + if (cp32 >= ' ' && cp32 != 0x7F){ + AppTypeCodepoint(cp32); + } + }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_CLOSE: + { + keep_running = 0; + }break; + + default: + { + result = DefWindowProcW(wnd, msg, wparam, lparam); + }break; + } + + return(result); +} + + +//////////////////////////////// +// 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); +} + + +//////////////////////////////// +// Main + +static BOOL composition_enabled = 0; + +int +WinMain(HINSTANCE hInstance, + HINSTANCE hPrevInstance, + LPSTR lpCmdLine, + int nShowCmd){ + // window setup +#define WINDOW_CLASS L"InputWindowClass" + + 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 = WINDOW_CLASS; + + 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(WINDOW_CLASS, L"Type Test", style, + CW_USEDEFAULT, CW_USEDEFAULT, + CW_USEDEFAULT, CW_USEDEFAULT, + 0, 0, hInstance, 0); + + // setup vsync + if (DwmIsCompositionEnabled(&composition_enabled) != S_OK){ + composition_enabled = 0; + } + if (!composition_enabled){ + MessageBoxW(0, (WCHAR*)L"No Vsync\n", (WCHAR*)L"Issue", MB_OK); + } + + // init + AppInit(wnd); + + // main loop + ShowWindow(wnd, SW_SHOW); + + keep_running = 1; + for (;keep_running;){ + + // flush message queue + { + BOOL wait_for_message = 1; + MSG msg; + for (;(wait_for_message? + GetMessageW(&msg, 0, 0, 0): + PeekMessageW(&msg, 0, 0, 0, PM_REMOVE));){ + wait_for_message = 0; + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + // render + AppRender(wnd); + + // vsync or sleep + if (composition_enabled){ + DwmFlush(); + } + else{ + Sleep(33); + } + } +} + +//$ graphical //