2021-01-27 15:52:39 +03:00
|
|
|
// Copyright (c) 2020-2021 Raúl Hernández. All rights reserved.
|
2020-11-26 02:28:57 +03:00
|
|
|
// Use of this source code is governed by an MIT license
|
|
|
|
// that can be found in the LICENSE file.
|
2020-11-12 14:12:51 +03:00
|
|
|
module ui
|
|
|
|
|
2021-01-27 15:52:39 +03:00
|
|
|
import os
|
|
|
|
import time
|
|
|
|
|
2021-09-06 19:04:14 +03:00
|
|
|
const buf_size = 64
|
|
|
|
|
|
|
|
const ctx_ptr = &Context(0)
|
|
|
|
|
|
|
|
const stdin_at_startup = u32(0)
|
2020-11-13 16:30:47 +03:00
|
|
|
|
2021-01-27 15:52:39 +03:00
|
|
|
struct ExtraContext {
|
|
|
|
mut:
|
|
|
|
stdin_handle C.HANDLE
|
|
|
|
stdout_handle C.HANDLE
|
|
|
|
read_buf [buf_size]C.INPUT_RECORD
|
|
|
|
mouse_down MouseButton
|
|
|
|
}
|
|
|
|
|
|
|
|
fn restore_terminal_state() {
|
2022-05-20 18:30:16 +03:00
|
|
|
if unsafe { ui.ctx_ptr != 0 } {
|
2021-05-08 13:32:29 +03:00
|
|
|
if ui.ctx_ptr.cfg.use_alternate_buffer {
|
2021-01-27 15:52:39 +03:00
|
|
|
// clear the terminal and set the cursor to the origin
|
|
|
|
print('\x1b[2J\x1b[3J')
|
|
|
|
print('\x1b[?1049l')
|
2022-05-30 19:15:05 +03:00
|
|
|
flush_stdout()
|
2021-01-27 15:52:39 +03:00
|
|
|
}
|
2021-05-08 13:32:29 +03:00
|
|
|
C.SetConsoleMode(ui.ctx_ptr.stdin_handle, ui.stdin_at_startup)
|
2021-01-27 15:52:39 +03:00
|
|
|
}
|
|
|
|
load_title()
|
|
|
|
os.flush()
|
|
|
|
}
|
|
|
|
|
2020-11-12 14:12:51 +03:00
|
|
|
pub fn init(cfg Config) &Context {
|
2021-01-27 15:52:39 +03:00
|
|
|
mut ctx := &Context{
|
|
|
|
cfg: cfg
|
|
|
|
}
|
|
|
|
// get the standard input handle
|
|
|
|
stdin_handle := C.GetStdHandle(C.STD_INPUT_HANDLE)
|
|
|
|
stdout_handle := C.GetStdHandle(C.STD_OUTPUT_HANDLE)
|
|
|
|
if stdin_handle == C.INVALID_HANDLE_VALUE {
|
|
|
|
panic('could not get stdin handle')
|
|
|
|
}
|
|
|
|
// save the current input mode, to be restored on exit
|
2021-05-08 13:32:29 +03:00
|
|
|
if C.GetConsoleMode(stdin_handle, &ui.stdin_at_startup) == 0 {
|
2021-01-27 15:52:39 +03:00
|
|
|
panic('could not get stdin console mode')
|
|
|
|
}
|
|
|
|
|
|
|
|
// enable extended input flags (see https://stackoverflow.com/a/46802726)
|
|
|
|
// 0x80 == C.ENABLE_EXTENDED_FLAGS
|
|
|
|
if C.SetConsoleMode(stdin_handle, 0x80) == 0 {
|
|
|
|
panic('could not set raw input mode')
|
|
|
|
}
|
|
|
|
// enable window and mouse input events.
|
|
|
|
if C.SetConsoleMode(stdin_handle, C.ENABLE_WINDOW_INPUT | C.ENABLE_MOUSE_INPUT) == 0 {
|
|
|
|
panic('could not set raw input mode')
|
|
|
|
}
|
|
|
|
// store the current title, so restore_terminal_state can get it back
|
|
|
|
save_title()
|
|
|
|
|
|
|
|
if ctx.cfg.use_alternate_buffer {
|
|
|
|
// switch to the alternate buffer
|
|
|
|
print('\x1b[?1049h')
|
|
|
|
// clear the terminal and set the cursor to the origin
|
|
|
|
print('\x1b[2J\x1b[3J\x1b[1;1H')
|
2022-05-30 19:15:05 +03:00
|
|
|
flush_stdout()
|
2021-01-27 15:52:39 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if ctx.cfg.hide_cursor {
|
2021-03-04 13:31:26 +03:00
|
|
|
ctx.hide_cursor()
|
|
|
|
ctx.flush()
|
2021-01-27 15:52:39 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if ctx.cfg.window_title != '' {
|
|
|
|
print('\x1b]0;$ctx.cfg.window_title\x07')
|
2022-05-30 19:15:05 +03:00
|
|
|
flush_stdout()
|
2021-01-27 15:52:39 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
unsafe {
|
2021-05-08 13:32:29 +03:00
|
|
|
x := &ui.ctx_ptr
|
2021-01-27 15:52:39 +03:00
|
|
|
*x = ctx
|
|
|
|
}
|
|
|
|
C.atexit(restore_terminal_state)
|
|
|
|
for code in ctx.cfg.reset {
|
2021-05-18 11:59:57 +03:00
|
|
|
os.signal_opt(code, fn (_ os.Signal) {
|
2022-06-21 13:23:21 +03:00
|
|
|
mut c := unsafe { ui.ctx_ptr }
|
2022-05-20 18:30:16 +03:00
|
|
|
if unsafe { c != 0 } {
|
2021-01-27 15:52:39 +03:00
|
|
|
c.cleanup()
|
|
|
|
}
|
|
|
|
exit(0)
|
2021-05-18 11:59:57 +03:00
|
|
|
}) or {}
|
2021-01-27 15:52:39 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
ctx.stdin_handle = stdin_handle
|
|
|
|
ctx.stdout_handle = stdout_handle
|
|
|
|
return ctx
|
2020-11-12 14:12:51 +03:00
|
|
|
}
|
|
|
|
|
2021-01-26 20:26:05 +03:00
|
|
|
pub fn (mut ctx Context) run() ? {
|
2021-01-27 15:52:39 +03:00
|
|
|
frame_time := 1_000_000 / ctx.cfg.frame_rate
|
|
|
|
mut init_called := false
|
|
|
|
mut sw := time.new_stopwatch(auto_start: false)
|
|
|
|
mut sleep_len := 0
|
|
|
|
for {
|
|
|
|
if !init_called {
|
|
|
|
ctx.init()
|
|
|
|
init_called = true
|
|
|
|
}
|
|
|
|
if sleep_len > 0 {
|
2021-02-27 20:41:06 +03:00
|
|
|
time.sleep(sleep_len * time.microsecond)
|
2021-01-27 15:52:39 +03:00
|
|
|
}
|
|
|
|
if !ctx.paused {
|
|
|
|
sw.restart()
|
|
|
|
if ctx.cfg.event_fn != voidptr(0) {
|
|
|
|
ctx.parse_events()
|
|
|
|
}
|
|
|
|
ctx.frame()
|
|
|
|
sw.pause()
|
|
|
|
e := sw.elapsed().microseconds()
|
|
|
|
sleep_len = frame_time - int(e)
|
|
|
|
ctx.frame_count++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn (mut ctx Context) parse_events() {
|
|
|
|
nr_events := u32(0)
|
|
|
|
if !C.GetNumberOfConsoleInputEvents(ctx.stdin_handle, &nr_events) {
|
|
|
|
panic('could not get number of events in stdin')
|
|
|
|
}
|
2021-05-08 13:32:29 +03:00
|
|
|
if nr_events < 1 {
|
|
|
|
return
|
|
|
|
}
|
2021-01-27 15:52:39 +03:00
|
|
|
|
|
|
|
// print('$nr_events | ')
|
2021-05-08 13:32:29 +03:00
|
|
|
if !C.ReadConsoleInput(ctx.stdin_handle, &ctx.read_buf[0], ui.buf_size, &nr_events) {
|
2021-01-27 15:52:39 +03:00
|
|
|
panic('could not read from stdin')
|
|
|
|
}
|
|
|
|
for i in 0 .. nr_events {
|
|
|
|
// print('E ')
|
|
|
|
match int(ctx.read_buf[i].EventType) {
|
|
|
|
C.KEY_EVENT {
|
|
|
|
e := unsafe { ctx.read_buf[i].Event.KeyEvent }
|
|
|
|
ch := e.wVirtualKeyCode
|
|
|
|
ascii := unsafe { e.uChar.AsciiChar }
|
2021-05-08 13:32:29 +03:00
|
|
|
if e.bKeyDown == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// we don't handle key_up events because they don't exist on linux...
|
2021-01-27 15:52:39 +03:00
|
|
|
// see: https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
|
|
|
code := match int(ch) {
|
|
|
|
C.VK_BACK { KeyCode.backspace }
|
|
|
|
C.VK_RETURN { KeyCode.enter }
|
|
|
|
C.VK_PRIOR { KeyCode.page_up }
|
2021-05-08 13:32:29 +03:00
|
|
|
14...20 { KeyCode.null }
|
2021-01-27 15:52:39 +03:00
|
|
|
C.VK_NEXT { KeyCode.page_down }
|
|
|
|
C.VK_END { KeyCode.end }
|
|
|
|
C.VK_HOME { KeyCode.home }
|
|
|
|
C.VK_LEFT { KeyCode.left }
|
|
|
|
C.VK_UP { KeyCode.up }
|
|
|
|
C.VK_RIGHT { KeyCode.right }
|
|
|
|
C.VK_DOWN { KeyCode.down }
|
|
|
|
C.VK_INSERT { KeyCode.insert }
|
|
|
|
C.VK_DELETE { KeyCode.delete }
|
2021-05-08 13:32:29 +03:00
|
|
|
65...90 { KeyCode(ch + 32) } // letters
|
|
|
|
91...93 { KeyCode.null } // special keys
|
|
|
|
96...105 { KeyCode(ch - 48) } // numpad numbers
|
|
|
|
112...135 { KeyCode(ch + 178) } // f1 - f24
|
2021-01-27 15:52:39 +03:00
|
|
|
else { KeyCode(ascii) }
|
|
|
|
}
|
|
|
|
|
2021-02-21 17:07:49 +03:00
|
|
|
mut modifiers := Modifiers{}
|
2021-05-08 13:32:29 +03:00
|
|
|
if e.dwControlKeyState & (0x1 | 0x2) != 0 {
|
|
|
|
modifiers.set(.alt)
|
|
|
|
}
|
|
|
|
if e.dwControlKeyState & (0x4 | 0x8) != 0 {
|
|
|
|
modifiers.set(.ctrl)
|
|
|
|
}
|
|
|
|
if e.dwControlKeyState & 0x10 != 0 {
|
|
|
|
modifiers.set(.shift)
|
|
|
|
}
|
2021-01-27 15:52:39 +03:00
|
|
|
|
|
|
|
mut event := &Event{
|
|
|
|
typ: .key_down
|
|
|
|
modifiers: modifiers
|
|
|
|
code: code
|
|
|
|
ascii: ascii
|
|
|
|
width: int(e.dwControlKeyState)
|
|
|
|
height: int(e.wVirtualKeyCode)
|
|
|
|
utf8: unsafe { e.uChar.UnicodeChar.str() }
|
|
|
|
}
|
|
|
|
ctx.event(event)
|
|
|
|
}
|
|
|
|
C.MOUSE_EVENT {
|
|
|
|
e := unsafe { ctx.read_buf[i].Event.MouseEvent }
|
|
|
|
sb_info := C.CONSOLE_SCREEN_BUFFER_INFO{}
|
|
|
|
if !C.GetConsoleScreenBufferInfo(ctx.stdout_handle, &sb_info) {
|
|
|
|
panic('could not get screenbuffer info')
|
|
|
|
}
|
2021-01-27 20:39:53 +03:00
|
|
|
x := e.dwMousePosition.X + 1
|
|
|
|
y := int(e.dwMousePosition.Y) - sb_info.srWindow.Top + 1
|
2021-02-21 17:07:49 +03:00
|
|
|
mut modifiers := Modifiers{}
|
2021-05-08 13:32:29 +03:00
|
|
|
if e.dwControlKeyState & (0x1 | 0x2) != 0 {
|
|
|
|
modifiers.set(.alt)
|
|
|
|
}
|
|
|
|
if e.dwControlKeyState & (0x4 | 0x8) != 0 {
|
|
|
|
modifiers.set(.ctrl)
|
|
|
|
}
|
|
|
|
if e.dwControlKeyState & 0x10 != 0 {
|
|
|
|
modifiers.set(.shift)
|
|
|
|
}
|
2021-01-27 15:52:39 +03:00
|
|
|
// TODO: handle capslock/numlock/etc?? events exist for those keys
|
|
|
|
match int(e.dwEventFlags) {
|
|
|
|
C.MOUSE_MOVED {
|
|
|
|
mut button := match int(e.dwButtonState) {
|
|
|
|
0 { MouseButton.unknown }
|
|
|
|
1 { MouseButton.left }
|
|
|
|
2 { MouseButton.right }
|
|
|
|
else { MouseButton.middle }
|
|
|
|
}
|
|
|
|
typ := if e.dwButtonState == 0 {
|
|
|
|
if ctx.mouse_down != .unknown {
|
|
|
|
button = ctx.mouse_down
|
|
|
|
ctx.mouse_down = .unknown
|
|
|
|
EventType.mouse_up
|
|
|
|
} else {
|
|
|
|
EventType.mouse_move
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
EventType.mouse_drag
|
|
|
|
}
|
|
|
|
ctx.event(&Event{
|
|
|
|
typ: typ
|
|
|
|
x: x
|
|
|
|
y: y
|
|
|
|
button: button
|
|
|
|
modifiers: modifiers
|
|
|
|
})
|
2021-05-08 13:32:29 +03:00
|
|
|
}
|
|
|
|
C.MOUSE_WHEELED {
|
2021-01-27 15:52:39 +03:00
|
|
|
ctx.event(&Event{
|
|
|
|
typ: .mouse_scroll
|
2021-05-08 13:32:29 +03:00
|
|
|
direction: if i16(e.dwButtonState >> 16) < 0 {
|
|
|
|
Direction.up
|
|
|
|
} else {
|
|
|
|
Direction.down
|
|
|
|
}
|
2021-01-27 15:52:39 +03:00
|
|
|
x: x
|
|
|
|
y: y
|
|
|
|
modifiers: modifiers
|
|
|
|
})
|
2021-05-08 13:32:29 +03:00
|
|
|
}
|
|
|
|
0x0008 /* C.MOUSE_HWHEELED */ {
|
2021-01-27 15:52:39 +03:00
|
|
|
ctx.event(&Event{
|
|
|
|
typ: .mouse_scroll
|
2021-05-08 13:32:29 +03:00
|
|
|
direction: if i16(e.dwButtonState >> 16) < 0 {
|
|
|
|
Direction.right
|
|
|
|
} else {
|
|
|
|
Direction.left
|
|
|
|
}
|
2021-01-27 15:52:39 +03:00
|
|
|
x: x
|
|
|
|
y: y
|
|
|
|
modifiers: modifiers
|
|
|
|
})
|
2021-05-08 13:32:29 +03:00
|
|
|
}
|
|
|
|
0 /* CLICK */, C.DOUBLE_CLICK {
|
2021-01-27 15:52:39 +03:00
|
|
|
button := match int(e.dwButtonState) {
|
|
|
|
0 { ctx.mouse_down }
|
|
|
|
1 { MouseButton.left }
|
|
|
|
2 { MouseButton.right }
|
|
|
|
else { MouseButton.middle }
|
|
|
|
}
|
|
|
|
ctx.mouse_down = button
|
|
|
|
ctx.event(&Event{
|
|
|
|
typ: .mouse_down
|
|
|
|
x: x
|
|
|
|
y: y
|
|
|
|
button: button
|
|
|
|
modifiers: modifiers
|
|
|
|
})
|
2021-05-08 13:32:29 +03:00
|
|
|
}
|
|
|
|
else {}
|
2021-01-27 15:52:39 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
C.WINDOW_BUFFER_SIZE_EVENT {
|
|
|
|
// e := unsafe { ctx.read_buf[i].Event.WindowBufferSizeEvent }
|
|
|
|
sb := C.CONSOLE_SCREEN_BUFFER_INFO{}
|
|
|
|
if !C.GetConsoleScreenBufferInfo(ctx.stdout_handle, &sb) {
|
|
|
|
panic('could not get screenbuffer info')
|
|
|
|
}
|
2022-04-18 21:30:09 +03:00
|
|
|
w := sb.srWindow.Right - sb.srWindow.Left + 1
|
|
|
|
h := sb.srWindow.Bottom - sb.srWindow.Top + 1
|
2021-01-27 15:52:39 +03:00
|
|
|
utf8 := '($ctx.window_width, $ctx.window_height) -> ($w, $h)'
|
|
|
|
if w != ctx.window_width || h != ctx.window_height {
|
|
|
|
ctx.window_width, ctx.window_height = w, h
|
|
|
|
mut event := &Event{
|
|
|
|
typ: .resized
|
|
|
|
width: ctx.window_width
|
|
|
|
height: ctx.window_height
|
|
|
|
utf8: utf8
|
|
|
|
}
|
|
|
|
ctx.event(event)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// C.MENU_EVENT {
|
|
|
|
// e := unsafe { ctx.read_buf[i].Event.MenuEvent }
|
|
|
|
// }
|
|
|
|
// C.FOCUS_EVENT {
|
|
|
|
// e := unsafe { ctx.read_buf[i].Event.FocusEvent }
|
|
|
|
// }
|
|
|
|
else {}
|
|
|
|
}
|
|
|
|
}
|
2020-11-13 16:30:47 +03:00
|
|
|
}
|
|
|
|
|
2021-01-27 15:52:39 +03:00
|
|
|
[inline]
|
|
|
|
fn save_title() {
|
2021-05-08 13:32:29 +03:00
|
|
|
// restore the previously saved terminal title
|
|
|
|
print('\x1b[22;0t')
|
2022-05-30 19:15:05 +03:00
|
|
|
flush_stdout()
|
2020-11-13 16:30:47 +03:00
|
|
|
}
|
|
|
|
|
2021-01-27 15:52:39 +03:00
|
|
|
[inline]
|
|
|
|
fn load_title() {
|
2021-05-08 13:32:29 +03:00
|
|
|
// restore the previously saved terminal title
|
|
|
|
print('\x1b[23;0t')
|
2022-05-30 19:15:05 +03:00
|
|
|
flush_stdout()
|
2020-11-12 14:12:51 +03:00
|
|
|
}
|