From cbb4b1f06b2395705851430742e23240ece90510 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 16 Feb 2024 02:33:20 +0000 Subject: Base implementation of TUI/windows. --- TUI/module.jai | 10 ++--- TUI/windows.jai | 116 ++++++++++++++++++++++++++++++++++---------------------- ttt.jai | 8 ++-- 3 files changed, 78 insertions(+), 56 deletions(-) diff --git a/TUI/module.jai b/TUI/module.jai index 0ae0398..9c90805 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -157,14 +157,13 @@ set_style :: (bold: bool, underline: bool = false, strike_through: bool = false, key/u64 |0|0|c|b|a| -> that in memory lays as (BE:|0|0|c|b|a|) and (LE:|a|b|c|0|0|) */ -Key :: u64; +Key :: u64; // Terminal key-codes have 1 to 6 bytes so we'll use 8 bytes. KEY_SIZE :: #run type_info(Key).runtime_size; Keys :: struct #type_info_none { - // Terminal key-codes have 1 to 6 bytes, so we can signal special cases setting the edge-bytes. - None : Key : 0xF0000000_0000000F; - Resize : Key : 0xF0000000_0000001F; + None : Key : #run to_key("#NONE"); + Resize : Key : #run to_key("#RESIZE"); Space : Key : #char " "; Enter : Key : #char "\r"; @@ -399,7 +398,6 @@ set_next_key :: (key: Key) { get_key :: (timeout_milliseconds: s32 = -1) -> Key { assert_is_initialized(); - /* TODO get_key already deals with utf8 codes, but we don't know when we're receiving ANSI escape codes. If initial key is escape and other keys are awaiting in the input buffer, we need to parse them as escaped sequences. See wikiedia* for help on that. Lets use the escape sequences used on windows amd forget all others. Those should be the most used ones; at least they are the cross-platform compatible ones :P @@ -429,7 +427,7 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { defer input_override = xx Keys.None; return input_override; } - + if OS_was_terminal_resized() return xx Keys.Resize; should_read_input := false; diff --git a/TUI/windows.jai b/TUI/windows.jai index ff4d6a1..f79a5cf 100644 --- a/TUI/windows.jai +++ b/TUI/windows.jai @@ -15,6 +15,7 @@ WORD :: u16; DWORD :: s32; LPDWORD :: *s32; + UINT :: u32; PINPUT_RECORD :: *INPUT_RECORD; @@ -107,6 +108,14 @@ dwMaximumWindowSize : COORD; } + INPUT_RECORD_EVENT_TYPE :: enum u16 { + KEY_EVENT :: 0x0001; + MOUSE_EVENT :: 0x0002; + WINDOW_BUFFER_SIZE_EVENT :: 0x0004; + MENU_EVENT :: 0x0008; + FOCUS_EVENT :: 0x0010; + } + INPUT_RECORD :: struct { EventType : INPUT_RECORD_EVENT_TYPE; union { @@ -114,23 +123,14 @@ MouseEvent : MOUSE_EVENT_RECORD; WindowBufferSizeEvent : WINDOW_BUFFER_SIZE_RECORD; // These events are used internally and should be ignored. - //MenuEvent : MENU_EVENT_RECORD; - //FocusEvent : FOCUS_EVENT_RECORD; + MenuEvent : MENU_EVENT_RECORD; + FocusEvent : FOCUS_EVENT_RECORD; } - //Event : EventUnion; } - INPUT_RECORD_EVENT_TYPE :: enum s32 { - KEY_EVENT :: 0x0001; - MOUSE_EVENT :: 0x0002; - WINDOW_BUFFER_SIZE_EVENT :: 0x0004; - MENU_EVENT :: 0x0008; - FOCUS_EVENT :: 0x0010; - } - KEY_EVENT_RECORD :: struct { bKeyDown : BOOL; - wRepeatCount : WORD; + wRepeatCount : WORD #align 4; wVirtualKeyCode : WORD; wVirtualScanCode : WORD; union { @@ -151,6 +151,14 @@ dwSize : COORD; } + MENU_EVENT_RECORD :: struct { + dwCommandId : UINT; + } + + FOCUS_EVENT_RECORD :: struct { + bSetFocus : BOOL; + } + stdin: HANDLE; initial_stdin_mode: u32; raw_stdin_mode: Console_Input_Mode; @@ -297,13 +305,23 @@ OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, error: bo assert(bytes_to_read <= 0x7fff_ffff, "The Windows API only allows to read up to s32 bytes from the standard input."); - bytes_read: s32; - success := ReadConsoleA(stdin, buffer, cast(s32)bytes_to_read, *bytes_read); - if success == false { - _, error_message := get_error_value_and_string(); - return -1, true, error_message; + bytes_read: s32 = 0; + available_inputs := count_input(); + + while bytes_to_read > 0 && available_inputs > 0 { + record := read_input(); + + if record.EventType == .WINDOW_BUFFER_SIZE_EVENT { + was_resized = true; + } + + if record.EventType == .KEY_EVENT && record.KeyEvent.bKeyDown == true { + buffer[bytes_read] = xx record.KeyEvent.AsciiChar; + bytes_to_read -= 1; + bytes_read += 1; + } + available_inputs -= 1; } - // print(">%:%<", bytes_to_read, bytes_read); TODO DEBUG return bytes_read; } @@ -311,23 +329,22 @@ OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, error: bo // 0: do not wait // -1: wait indefinitely OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: bool { - /* TODO - Try to implement using: - https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobjectex - This wait procedure on windows should check if next input is valid (keyboard down) before returning, otherwise it should discar that input and go back to wait (if more sleep is allowed). + /* TODO + Add a good comment explaining how the windows part of the module was implemented... what's the idea behind it. + Something like, Since windows provides a single input buffer with all events, we need to peek through them and + discard the ones that are of no use for us. + Because it's a single buffer, all functions need to do repeated work (see if it's resize, see if it's a key press) + ... and so on. */ - + expiration := current_time_monotonic() + to_apollo(timeout_milliseconds / 1000.0); - // Possible values for poll_return TODO NOT BEING USED - WAIT_ABANDONED :: 0x00000080; - //WAIT_IO_COMPLETION :: 0x000000C0; - WAIT_OBJECT_0 :: 0x00000000; - WAIT_TIMEOUT :: 0x00000102; - WAIT_FAILED :: 0xFFFFFFFF; - //return ifx poll_return == WAIT_OBJECT_0 then true else false; + WAIT_ABANDONED :: 0x00000080; // Mutex stuff. + WAIT_OBJECT_0 :: 0x00000000; // Detected input. + WAIT_TIMEOUT :: 0x00000102; // Reached timeout. + WAIT_FAILED :: 0xFFFFFFFF; // Something went wrong. while true { poll_return := WaitForSingleObject(stdin, timeout_milliseconds); @@ -343,19 +360,33 @@ OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: boo _, error_message := get_error_value_and_string(); assert(false, error_message); } -// if poll_return != WAIT_OBJECT_0 then return false; - // Discard invalid input until a valid input is found or no input is left. + // Discard invalid input events. count := count_input(); while count > 0 { record := peek_input(); - if record.EventType == .KEY_EVENT && record.KeyEvent.bKeyDown == true || record.EventType == .WINDOW_BUFFER_SIZE_EVENT { + + if record.EventType == .WINDOW_BUFFER_SIZE_EVENT { + // Discard any additional resize event. + while peek_input().EventType == .WINDOW_BUFFER_SIZE_EVENT { + was_resized = true; + read_input(); + } + return false; + } + + if record.EventType == .KEY_EVENT && record.KeyEvent.bKeyDown == true { return true; } - read_input(); // TODO Discard input. + + read_input(); count -= 1; } - + + // When waiting indefinitely... just continue. + if timeout_milliseconds < 0 then continue; + + // Either break due to timeout, or update the remaining timeout. now := current_time_monotonic(); if now >= expiration then return false; timeout_milliseconds = xx to_milliseconds(expiration - now); @@ -365,18 +396,11 @@ OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: boo } OS_was_terminal_resized :: () -> bool { - - defer was_resized = false; // TODO Not using this flag... what happens if ... not sure what... :thinking: - - flag: = false; - - record := peek_input(); - while record.EventType == .WINDOW_BUFFER_SIZE_EVENT { + while peek_input().EventType == .WINDOW_BUFFER_SIZE_EVENT { + was_resized = true; read_input(); - record = peek_input(); - flag = true; } - //return was_resized; - return flag; + defer was_resized = false; + return was_resized; } diff --git a/ttt.jai b/ttt.jai index 8987a77..072d671 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1280,15 +1280,14 @@ main :: () { if 1 { print("TEST : draw box\n", to_standard_error = true); auto_release_temp(); - TUI.start(); + TUI.start(); // TODO Should start() call flush_input internally? + TUI.flush_input(); TUI.clear_terminal(); TUI.draw_box(1, 2, 5, 3); TUI.set_cursor_position(1, 1); print("Can you see the box below? (y/n)"); key := TUI.get_key(); TUI.stop(); - - print("\n\n\rDEBUG DEBUG WIP WIP WIP TODO HACK\n\n\r>%<\n\r", TUI.to_string(key)); // TODO WIP Currently debugging this. assert(key == #char "y", "# Failed to draw box.\n"); print("- success\n", to_standard_error = true); } @@ -1325,7 +1324,8 @@ main :: () { assert(key == #char "y", "# Failed to set terminal title.\n"); print("- success\n", to_standard_error = true); } - + + // TODO Setup this test... check for Meta+FX or Ctrl+FX compatibility between windows and *nix. if 1 { print("TEST : print keys and set terminal title\n", to_standard_error = true); TUI.start(); -- cgit v1.2.3