v2 / vlib / term / ui / input_windows.c.v
344 lines · 322 sloc · 8.59 KB · 8e4dcc699acd2e529cb14313f265aebabfd59c28
Raw
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]
5module ui
6
7import os
8import time
9
10const buf_size = 64
11
12__global ctx_ptr = &Context(unsafe { nil })
13
14__global stdin_at_startup = u32(0)
15
16struct ExtraContext {
17mut:
18 stdin_handle C.HANDLE
19 stdout_handle C.HANDLE
20 read_buf [buf_size]C.INPUT_RECORD
21 mouse_down MouseButton
22}
23
24fn 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`.
39pub 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.
106pub 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
133fn (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]
333fn save_title() {
334 // restore the previously saved terminal title
335 print('\x1b[22;0t')
336 flush_stdout()
337}
338
339@[inline]
340fn load_title() {
341 // restore the previously saved terminal title
342 print('\x1b[23;0t')
343 flush_stdout()
344}
345