| 1 | // Copyright (c) 2020-2024 Raúl Hernández. All rights reserved. |
| 2 | // Use of this source code is governed by an MIT license |
| 3 | // that can be found in the LICENSE file. |
| 4 | @[has_globals] |
| 5 | module ui |
| 6 | |
| 7 | import os |
| 8 | import time |
| 9 | |
| 10 | const buf_size = 64 |
| 11 | |
| 12 | __global ctx_ptr = &Context(unsafe { nil }) |
| 13 | |
| 14 | __global stdin_at_startup = u32(0) |
| 15 | |
| 16 | struct ExtraContext { |
| 17 | mut: |
| 18 | stdin_handle C.HANDLE |
| 19 | stdout_handle C.HANDLE |
| 20 | read_buf [buf_size]C.INPUT_RECORD |
| 21 | mouse_down MouseButton |
| 22 | } |
| 23 | |
| 24 | fn restore_terminal_state() { |
| 25 | if unsafe { ctx_ptr != 0 } { |
| 26 | if ctx_ptr.cfg.use_alternate_buffer { |
| 27 | // clear the terminal and set the cursor to the origin |
| 28 | print('\x1b[2J\x1b[3J') |
| 29 | print('\x1b[?1049l') |
| 30 | flush_stdout() |
| 31 | } |
| 32 | C.SetConsoleMode(ctx_ptr.stdin_handle, stdin_at_startup) |
| 33 | } |
| 34 | load_title() |
| 35 | os.flush() |
| 36 | } |
| 37 | |
| 38 | // init initializes the context of a windows console given the `cfg`. |
| 39 | pub fn init(cfg Config) &Context { |
| 40 | mut ctx := &Context{ |
| 41 | cfg: cfg |
| 42 | } |
| 43 | // get the standard input handle |
| 44 | stdin_handle := C.GetStdHandle(C.STD_INPUT_HANDLE) |
| 45 | stdout_handle := C.GetStdHandle(C.STD_OUTPUT_HANDLE) |
| 46 | if stdin_handle == C.INVALID_HANDLE_VALUE { |
| 47 | panic('could not get stdin handle') |
| 48 | } |
| 49 | // save the current input mode, to be restored on exit |
| 50 | if !C.GetConsoleMode(stdin_handle, &stdin_at_startup) { |
| 51 | panic('could not get stdin console mode') |
| 52 | } |
| 53 | |
| 54 | // enable extended input flags (see https://stackoverflow.com/a/46802726) |
| 55 | // 0x80 == C.ENABLE_EXTENDED_FLAGS |
| 56 | if !C.SetConsoleMode(stdin_handle, 0x80) { |
| 57 | panic('could not set raw input mode') |
| 58 | } |
| 59 | mut input_mode := u32(C.ENABLE_WINDOW_INPUT) |
| 60 | if ctx.cfg.mouse_enabled { |
| 61 | input_mode |= u32(C.ENABLE_MOUSE_INPUT) |
| 62 | } |
| 63 | // enable window input and optionally mouse input events. |
| 64 | if !C.SetConsoleMode(stdin_handle, input_mode) { |
| 65 | panic('could not set raw input mode') |
| 66 | } |
| 67 | // store the current title, so restore_terminal_state can get it back |
| 68 | save_title() |
| 69 | |
| 70 | if ctx.cfg.use_alternate_buffer { |
| 71 | // switch to the alternate buffer |
| 72 | print('\x1b[?1049h') |
| 73 | // clear the terminal and set the cursor to the origin |
| 74 | print('\x1b[2J\x1b[3J\x1b[1;1H') |
| 75 | flush_stdout() |
| 76 | } |
| 77 | |
| 78 | if ctx.cfg.hide_cursor { |
| 79 | ctx.hide_cursor() |
| 80 | ctx.flush() |
| 81 | } |
| 82 | |
| 83 | if ctx.cfg.window_title != '' { |
| 84 | print('\x1b]0;${ctx.cfg.window_title}\x07') |
| 85 | flush_stdout() |
| 86 | } |
| 87 | |
| 88 | ctx_ptr = ctx |
| 89 | at_exit(restore_terminal_state) or {} |
| 90 | for code in ctx.cfg.reset { |
| 91 | os.signal_opt(code, fn (_ os.Signal) { |
| 92 | mut c := ctx_ptr |
| 93 | if unsafe { c != 0 } { |
| 94 | c.cleanup() |
| 95 | } |
| 96 | exit(0) |
| 97 | }) or {} |
| 98 | } |
| 99 | |
| 100 | ctx.stdin_handle = stdin_handle |
| 101 | ctx.stdout_handle = stdout_handle |
| 102 | return ctx |
| 103 | } |
| 104 | |
| 105 | // run starts the windows console or restarts if it was paused. |
| 106 | pub fn (mut ctx Context) run() ! { |
| 107 | frame_time := 1_000_000 / ctx.cfg.frame_rate |
| 108 | mut init_called := false |
| 109 | mut sw := time.new_stopwatch(auto_start: false) |
| 110 | mut sleep_len := 0 |
| 111 | for { |
| 112 | if !init_called { |
| 113 | ctx.init() |
| 114 | init_called = true |
| 115 | } |
| 116 | if sleep_len > 0 { |
| 117 | time.sleep(sleep_len * time.microsecond) |
| 118 | } |
| 119 | if !ctx.paused { |
| 120 | sw.restart() |
| 121 | if ctx.cfg.event_fn != none { |
| 122 | ctx.parse_events() |
| 123 | } |
| 124 | ctx.frame() |
| 125 | sw.pause() |
| 126 | e := sw.elapsed().microseconds() |
| 127 | sleep_len = frame_time - int(e) |
| 128 | ctx.frame_count++ |
| 129 | } |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | fn (mut ctx Context) parse_events() { |
| 134 | nr_events := u32(0) |
| 135 | if !C.GetNumberOfConsoleInputEvents(ctx.stdin_handle, &nr_events) { |
| 136 | panic('could not get number of events in stdin') |
| 137 | } |
| 138 | if nr_events < 1 { |
| 139 | return |
| 140 | } |
| 141 | |
| 142 | // print('${nr_events} | ') |
| 143 | if !C.ReadConsoleInput(ctx.stdin_handle, &ctx.read_buf[0], buf_size, &nr_events) { |
| 144 | panic('could not read from stdin') |
| 145 | } |
| 146 | for i in 0 .. nr_events { |
| 147 | // print('E ') |
| 148 | match int(ctx.read_buf[i].EventType) { |
| 149 | C.KEY_EVENT { |
| 150 | e := unsafe { ctx.read_buf[i].Event.KeyEvent } |
| 151 | ch := e.wVirtualKeyCode |
| 152 | ascii := unsafe { e.uChar.AsciiChar } |
| 153 | code := match int(ch) { |
| 154 | C.VK_BACK { KeyCode.backspace } |
| 155 | C.VK_RETURN { KeyCode.enter } |
| 156 | C.VK_PRIOR { KeyCode.page_up } |
| 157 | 14...20 { KeyCode.null } |
| 158 | C.VK_NEXT { KeyCode.page_down } |
| 159 | C.VK_END { KeyCode.end } |
| 160 | C.VK_HOME { KeyCode.home } |
| 161 | C.VK_LEFT { KeyCode.left } |
| 162 | C.VK_UP { KeyCode.up } |
| 163 | C.VK_RIGHT { KeyCode.right } |
| 164 | C.VK_DOWN { KeyCode.down } |
| 165 | C.VK_INSERT { KeyCode.insert } |
| 166 | C.VK_DELETE { KeyCode.delete } |
| 167 | 65...90 { unsafe { KeyCode(ch + 32) } } // letters |
| 168 | 91...93 { KeyCode.null } // special keys |
| 169 | 96...105 { unsafe { KeyCode(ch - 48) } } // numpad numbers |
| 170 | 112...135 { unsafe { KeyCode(ch + 178) } } // f1 - f24 |
| 171 | else { unsafe { KeyCode(ascii) } } |
| 172 | } |
| 173 | |
| 174 | mut modifiers := unsafe { Modifiers(0) } |
| 175 | if e.dwControlKeyState & (0x1 | 0x2) != 0 { |
| 176 | modifiers.set(.alt) |
| 177 | } |
| 178 | if e.dwControlKeyState & (0x4 | 0x8) != 0 { |
| 179 | modifiers.set(.ctrl) |
| 180 | } |
| 181 | if e.dwControlKeyState & 0x10 != 0 { |
| 182 | modifiers.set(.shift) |
| 183 | } |
| 184 | |
| 185 | event_type := if e.bKeyDown == 0 { |
| 186 | EventType.key_up |
| 187 | } else { |
| 188 | EventType.key_down |
| 189 | } |
| 190 | repeat_count := if event_type == .key_down && e.wRepeatCount > 0 { |
| 191 | int(e.wRepeatCount) |
| 192 | } else { |
| 193 | 1 |
| 194 | } |
| 195 | for _ in 0 .. repeat_count { |
| 196 | mut event := &Event{ |
| 197 | typ: event_type |
| 198 | modifiers: modifiers |
| 199 | code: code |
| 200 | ascii: ascii |
| 201 | width: int(e.dwControlKeyState) |
| 202 | height: int(e.wVirtualKeyCode) |
| 203 | utf8: unsafe { e.uChar.UnicodeChar.str() } |
| 204 | } |
| 205 | ctx.event(event) |
| 206 | } |
| 207 | } |
| 208 | C.MOUSE_EVENT { |
| 209 | e := unsafe { ctx.read_buf[i].Event.MouseEvent } |
| 210 | sb_info := C.CONSOLE_SCREEN_BUFFER_INFO{} |
| 211 | if !C.GetConsoleScreenBufferInfo(ctx.stdout_handle, &sb_info) { |
| 212 | panic('could not get screenbuffer info') |
| 213 | } |
| 214 | x := e.dwMousePosition.X + 1 |
| 215 | y := int(e.dwMousePosition.Y) - sb_info.srWindow.Top + 1 |
| 216 | mut modifiers := unsafe { Modifiers(0) } |
| 217 | if e.dwControlKeyState & (0x1 | 0x2) != 0 { |
| 218 | modifiers.set(.alt) |
| 219 | } |
| 220 | if e.dwControlKeyState & (0x4 | 0x8) != 0 { |
| 221 | modifiers.set(.ctrl) |
| 222 | } |
| 223 | if e.dwControlKeyState & 0x10 != 0 { |
| 224 | modifiers.set(.shift) |
| 225 | } |
| 226 | // TODO: handle capslock/numlock/etc?? events exist for those keys |
| 227 | match int(e.dwEventFlags) { |
| 228 | C.MOUSE_MOVED { |
| 229 | mut button := match int(e.dwButtonState) { |
| 230 | 0 { MouseButton.unknown } |
| 231 | 1 { MouseButton.left } |
| 232 | 2 { MouseButton.right } |
| 233 | else { MouseButton.middle } |
| 234 | } |
| 235 | |
| 236 | typ := if e.dwButtonState == 0 { |
| 237 | if ctx.mouse_down != .unknown { |
| 238 | button = ctx.mouse_down |
| 239 | ctx.mouse_down = .unknown |
| 240 | EventType.mouse_up |
| 241 | } else { |
| 242 | EventType.mouse_move |
| 243 | } |
| 244 | } else { |
| 245 | EventType.mouse_drag |
| 246 | } |
| 247 | ctx.event(&Event{ |
| 248 | typ: typ |
| 249 | x: x |
| 250 | y: y |
| 251 | button: button |
| 252 | modifiers: modifiers |
| 253 | }) |
| 254 | } |
| 255 | C.MOUSE_WHEELED { |
| 256 | ctx.event(&Event{ |
| 257 | typ: .mouse_scroll |
| 258 | direction: if i16(e.dwButtonState >> 16) < 0 { |
| 259 | Direction.up |
| 260 | } else { |
| 261 | Direction.down |
| 262 | } |
| 263 | x: x |
| 264 | y: y |
| 265 | modifiers: modifiers |
| 266 | }) |
| 267 | } |
| 268 | 0x0008 { // C.MOUSE_HWHEELED |
| 269 | ctx.event(&Event{ |
| 270 | typ: .mouse_scroll |
| 271 | direction: if i16(e.dwButtonState >> 16) < 0 { |
| 272 | Direction.right |
| 273 | } else { |
| 274 | Direction.left |
| 275 | } |
| 276 | x: x |
| 277 | y: y |
| 278 | modifiers: modifiers |
| 279 | }) |
| 280 | } |
| 281 | 0, C.DOUBLE_CLICK { |
| 282 | button := match int(e.dwButtonState) { |
| 283 | 0 { ctx.mouse_down } |
| 284 | 1 { MouseButton.left } |
| 285 | 2 { MouseButton.right } |
| 286 | else { MouseButton.middle } |
| 287 | } |
| 288 | |
| 289 | ctx.mouse_down = button |
| 290 | ctx.event(&Event{ |
| 291 | typ: .mouse_down |
| 292 | x: x |
| 293 | y: y |
| 294 | button: button |
| 295 | modifiers: modifiers |
| 296 | }) |
| 297 | } |
| 298 | else {} |
| 299 | } |
| 300 | } |
| 301 | C.WINDOW_BUFFER_SIZE_EVENT { |
| 302 | // e := unsafe { ctx.read_buf[i].Event.WindowBufferSizeEvent } |
| 303 | sb := C.CONSOLE_SCREEN_BUFFER_INFO{} |
| 304 | if !C.GetConsoleScreenBufferInfo(ctx.stdout_handle, &sb) { |
| 305 | panic('could not get screenbuffer info') |
| 306 | } |
| 307 | w := sb.srWindow.Right - sb.srWindow.Left + 1 |
| 308 | h := sb.srWindow.Bottom - sb.srWindow.Top + 1 |
| 309 | utf8 := '(${ctx.window_width}, ${ctx.window_height}) -> (${w}, ${h})' |
| 310 | if w != ctx.window_width || h != ctx.window_height { |
| 311 | ctx.window_width, ctx.window_height = w, h |
| 312 | mut event := &Event{ |
| 313 | typ: .resized |
| 314 | width: ctx.window_width |
| 315 | height: ctx.window_height |
| 316 | utf8: utf8 |
| 317 | } |
| 318 | ctx.event(event) |
| 319 | } |
| 320 | } |
| 321 | // C.MENU_EVENT { |
| 322 | // e := unsafe { ctx.read_buf[i].Event.MenuEvent } |
| 323 | // } |
| 324 | // C.FOCUS_EVENT { |
| 325 | // e := unsafe { ctx.read_buf[i].Event.FocusEvent } |
| 326 | // } |
| 327 | else {} |
| 328 | } |
| 329 | } |
| 330 | } |
| 331 | |
| 332 | @[inline] |
| 333 | fn save_title() { |
| 334 | // restore the previously saved terminal title |
| 335 | print('\x1b[22;0t') |
| 336 | flush_stdout() |
| 337 | } |
| 338 | |
| 339 | @[inline] |
| 340 | fn load_title() { |
| 341 | // restore the previously saved terminal title |
| 342 | print('\x1b[23;0t') |
| 343 | flush_stdout() |
| 344 | } |
| 345 | |