win32_typing_test/win32_typing_test.c

342 lines
8.1 KiB
C

/*
** 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 <Windows.h>
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 //