aboutsummaryrefslogtreecommitdiff
path: root/code/linux_handmade.cpp
diff options
context:
space:
mode:
authorRaymaekers Luca <luca@spacehb.net>2025-07-24 11:05:50 +0200
committerRaymaekers Luca <luca@spacehb.net>2025-07-24 11:11:23 +0200
commite7dc403c36d5958238691578610d442a27ecc943 (patch)
tree95ad6a7f74acf736c5d423c8742496761dd436e7 /code/linux_handmade.cpp
parent4ab197add10847d4ba3036ed525f96db00a9849f (diff)
checkpoint
Diffstat (limited to 'code/linux_handmade.cpp')
-rw-r--r--code/linux_handmade.cpp200
1 files changed, 184 insertions, 16 deletions
diff --git a/code/linux_handmade.cpp b/code/linux_handmade.cpp
index ba75aed..0ed3b1d 100644
--- a/code/linux_handmade.cpp
+++ b/code/linux_handmade.cpp
@@ -21,9 +21,12 @@
#include "handmade_platform.h"
#include "linux_handmade.h"
+#include "x11_keysym_convert.c"
+
#define true 1
#define false 0
+#define ALSA_RECOVER_SILENT true
#define MAX_PLAYER_COUNT 4
#ifdef Assert
@@ -45,7 +48,6 @@ global_variable b32 GlobalPaused;
void MemCpy(char *Dest, char *Source, size_t Count)
{
while(Count--) *Dest++ = *Source++;
-
}
void MemSet(char *Dest, char Value, size_t Count)
@@ -81,6 +83,47 @@ void CatStrings(size_t SourceACount, char *SourceA,
}
}
+internal rune
+ConvertUTF8StringToRune(u8 UTF8String[4])
+{
+ rune Codepoint = 0;
+
+ if((UTF8String[0] & 0x80) == 0x00)
+ {
+ Codepoint = UTF8String[0];
+ }
+ else if((UTF8String[0] & 0xE0) == 0xC0)
+ {
+ Codepoint = (
+ ((UTF8String[0] & 0x1F) << 6*1) |
+ ((UTF8String[1] & 0x3F) << 6*0)
+ );
+ }
+ else if((UTF8String[0] & 0xF0) == 0xE0)
+ {
+ Codepoint = (
+ ((UTF8String[0] & 0x0F) << 6*2) |
+ ((UTF8String[1] & 0x3F) << 6*1) |
+ ((UTF8String[2] & 0x3F) << 6*0)
+ );
+ }
+ else if((UTF8String[0] & 0xF8) == 0xF8)
+ {
+ Codepoint = (
+ ((UTF8String[0] & 0x0E) << 6*3) |
+ ((UTF8String[1] & 0x3F) << 6*2) |
+ ((UTF8String[2] & 0x3F) << 6*1) |
+ ((UTF8String[3] & 0x3F) << 6*0)
+ );
+ }
+ else
+ {
+ Assert(0);
+ }
+
+ return Codepoint;
+}
+
struct linux_init_alsa_result
{
snd_pcm_t *PCMHandle;
@@ -414,7 +457,6 @@ internal void LinuxEndInputPlayBack(linux_state *State)
}
State->InputPlayingIndex = 0;
-
}
internal void LinuxRecordInput(linux_state *State, game_input *Input)
@@ -466,20 +508,124 @@ internal void LinuxShowCursor(Display *DisplayHandle, Window WindowHandle)
}
internal void LinuxProcessPendingMessages(Display *DisplayHandle, Window WindowHandle,
- Atom WM_DELETE_WINDOW, linux_state *State, game_controller_input *KeyboardController)
+ XIC InputContext, Atom WM_DELETE_WINDOW, linux_state *State, game_controller_input *KeyboardController)
{
XEvent WindowEvent = {};
while(XPending(DisplayHandle) > 0)
{
XNextEvent(DisplayHandle, &WindowEvent);
- switch (WindowEvent.type)
+ b32 FilteredEvent = XFilterEvent(&WindowEvent, WindowHandle);
+ if(FilteredEvent)
+ {
+ Assert(WindowEvent.type == KeyPress || WindowEvent.type == KeyRelease);
+ }
+
+ switch(WindowEvent.type)
{
case KeyPress:
case KeyRelease:
{
+ //- How text input works
+ // The needs:
+ // 1. Preserve game buttons, so that we can switch between a "game mode" or
+ // "text input mode".
+ // 2. Text input using the input method of the user which should allow for utf8 characters.
+ // 3. Hotkey support. Eg. quickly navigating text.
+ // 3 will be supported by 2 for code reuse.
+ //
+ // We are going to send a buffer text button presses to the game layer, this solves these
+ // issues:
+ // - Pressing the same key multiple times in one frame.
+ // - Having modifiers be specific to each key press.
+ // - Not having to create a button record for each possible character in the structure.
+ // - Key events come in one at a time in the event loop, thus we need to have a buffer for
+ // multiple keys pressed on a single frame.
+ //
+ // We store a count along the buffer and in the buffer we store the utf8 codepoint and its
+ // modifiers.
+ // The app code is responsible for traversing this buffer and applying the logic.
+
+ // The problem of input methods and hotkeys:
+ // Basically the problem is that if we allow the input method and combo's that could be
+ // filtered by the input method it won't seem consistent to the user.
+ // So we don't allow key bound to the input method to have an effect and we only pass key
+ // inputs that have not been filtered.
+ //
+ // In the platform layer we handle the special case were the input methods creates non-
+ // printable characters and we decompose those key inputs since non-printable characters
+ // have no use anymore.
+
+ // Extra:
+ // - I refuse to check which keys bind to what modifiers. It's not important.
+
+ // - Handy resources:
+ // - https://www.coderstool.com/unicode-text-converter
+ // - man Compose(5).
+ // - https://en.wikipedia.org/wiki/Control_key#History
+
KeySym Symbol = XLookupKeysym(&WindowEvent.xkey, 0);
b32 IsDown = (WindowEvent.type == KeyPress);
+ // TODO(luca): Refresh mappings.
+ // NOTE(luca): Only KeyPress events see man page of Xutf8LookupString(). And skip filtered events for text input, but keep them for controller.
+ if(IsDown && !FilteredEvent)
+ {
+ rune Codepoint = 'e';
+ u8 LookupBuffer[4] = {};
+ Status LookupStatus = {};
+
+ s32 BytesLookepdUp = Xutf8LookupString(InputContext, &WindowEvent.xkey,
+ (char *)&LookupBuffer, ArrayCount(LookupBuffer),
+ 0, &LookupStatus);
+ Assert(LookupStatus != XBufferOverflow);
+ Assert(BytesLookepdUp <= 4);
+
+ if(LookupStatus!= XLookupNone &&
+ LookupStatus!= XLookupKeySym)
+ {
+ if(BytesLookepdUp)
+ {
+ Assert(KeyboardController->Keyboard.TextInputCount < ArrayCount(KeyboardController->Keyboard.TextInputBuffer));
+
+ Codepoint = ConvertUTF8StringToRune(LookupBuffer);
+
+ // NOTE(luca): Input methods might produce non printable characters (< ' '). If this
+ // happens we try to "decompose" the key input.
+ if(Codepoint < ' ' && Codepoint >= 0)
+ {
+ if(Symbol >= XK_space)
+ {
+ Codepoint = (char)(' ' + (Symbol - XK_space));
+ }
+ }
+
+
+ if(Codepoint >= ' ' || Codepoint < 0)
+ {
+ game_text_button *TextButton = &KeyboardController->Keyboard.TextInputBuffer[KeyboardController->Keyboard.TextInputCount++];
+ TextButton->Codepoint = Codepoint;
+ TextButton->Shift = (WindowEvent.xkey.state & ShiftMask);
+ TextButton->Control = (WindowEvent.xkey.state & ControlMask);
+ TextButton->Alt = (WindowEvent.xkey.state & Mod1Mask);
+#if 0
+ printf("%d bytes '%c' %d (%c|%c|%c)\n",
+ BytesLookepdUp,
+ ((Codepoint >= ' ') ? (char)Codepoint : '\0'),
+ Codepoint,
+ ((WindowEvent.xkey.state & ShiftMask) ? 'S' : ' '),
+ ((WindowEvent.xkey.state & ControlMask) ? 'C' : ' '),
+ ((WindowEvent.xkey.state & Mod1Mask) ? 'A' : ' '));
+#endif
+ }
+ else
+ {
+ // TODO(luca): Logging
+ }
+
+ }
+ }
+ }
+
if(0) {}
else if(Symbol == XK_w)
{
@@ -517,14 +663,16 @@ internal void LinuxProcessPendingMessages(Display *DisplayHandle, Window WindowH
{
LinuxProcessKeyPress(&KeyboardController->Start, IsDown);
}
- else if(Symbol == XK_p)
+ else if((WindowEvent.xkey.state & Mod1Mask) &&
+ (Symbol == XK_p))
{
if(IsDown)
{
GlobalPaused = !GlobalPaused;
}
}
- else if(Symbol == XK_l)
+ else if((WindowEvent.xkey.state & Mod1Mask) &&
+ (Symbol == XK_l))
{
if(IsDown)
{
@@ -542,7 +690,8 @@ internal void LinuxProcessPendingMessages(Display *DisplayHandle, Window WindowH
}
else
{
- // TODO(luca): Reset buttons so they aren't held?
+ // NOTE(luca) Reset buttons so they aren't held.
+ // TODO(luca): Check if still needed since we clear halftransition counts.
for(u32 ButtonIndex = 0;
ButtonIndex < ArrayCount(KeyboardController->Buttons);
ButtonIndex++)
@@ -553,8 +702,8 @@ internal void LinuxProcessPendingMessages(Display *DisplayHandle, Window WindowH
}
}
}
- else if(Symbol == XK_Escape ||
- Symbol == XK_q)
+ else if((WindowEvent.xkey.state & Mod1Mask) &&
+ (Symbol == XK_F4))
{
GlobalRunning = false;
}
@@ -588,11 +737,8 @@ internal void LinuxProcessPendingMessages(Display *DisplayHandle, Window WindowH
{
//LinuxShowCursor(DisplayHandle, WindowHandle);
} break;
-
}
-
}
-
}
internal void LinuxSetSizeHint(Display *DisplayHandle, Window WindowHandle,
@@ -841,6 +987,20 @@ int main(int ArgC, char *Args[])
ClassHint.res_class = "Handmade Window";
XSetClassHint(DisplayHandle, WindowHandle, &ClassHint);
+ XSetLocaleModifiers("");
+
+ XIM InputMethod = XOpenIM(DisplayHandle, 0, 0, 0);
+ if(!InputMethod){
+ XSetLocaleModifiers("@im=none");
+ InputMethod = XOpenIM(DisplayHandle, 0, 0, 0);
+ }
+ XIC InputContext = XCreateIC(InputMethod,
+ XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
+ XNClientWindow, WindowHandle,
+ XNFocusWindow, WindowHandle,
+ NULL);
+ XSetICFocus(InputContext);
+
int BitsPerPixel = 32;
int BytesPerPixel = BitsPerPixel/8;
int WindowBufferSize = Width*Height*BytesPerPixel;
@@ -990,7 +1150,7 @@ int main(int ArgC, char *Args[])
{
NewInput->dtForFrame = TargetSecondsPerFrame;
-#if HANDMADE_INTERNAL
+#if HANDMADE_SLOW
// NOTE(luca): Because gcc will first create an empty file and then write into it we skip trying to reload when the file is empty.
struct stat FileStats = {};
stat(LibraryFullPath, &FileStats);
@@ -1010,7 +1170,16 @@ int main(int ArgC, char *Args[])
game_controller_input *NewKeyboardController = GetController(NewInput, 0);
NewKeyboardController->IsConnected = true;
- LinuxProcessPendingMessages(DisplayHandle, WindowHandle, WM_DELETE_WINDOW, &LinuxState, NewKeyboardController);
+ NewKeyboardController->Keyboard.TextInputCount = 0;
+ for(u32 ButtonIndex = 0;
+ ButtonIndex < ArrayCount(NewKeyboardController->Buttons);
+ ButtonIndex++)
+ {
+ NewKeyboardController->Buttons[ButtonIndex].HalfTransitionCount = 0;
+ }
+
+ LinuxProcessPendingMessages(DisplayHandle, WindowHandle, InputContext, WM_DELETE_WINDOW,
+ &LinuxState, NewKeyboardController);
// TODO(luca): Use buttonpress/release events instead so we query this less frequently.
s32 MouseX = 0, MouseY = 0, MouseZ = 0; // TODO(luca): Support mousewheel?
@@ -1127,7 +1296,6 @@ int main(int ArgC, char *Args[])
#endif
}
}
-
}
}
}
@@ -1229,7 +1397,7 @@ TODO
{
// TODO(luca): Logging
// NOTE(luca): We might want to silence in case of overruns ahead of time. We also probably want to handle latency differently here.
- snd_pcm_recover(PCMHandle, LastFramesWritten, 0);
+ snd_pcm_recover(PCMHandle, LastFramesWritten, ALSA_RECOVER_SILENT);
// underrun
if(LastFramesWritten == -EPIPE)