diff options
author | Indrajith K L | 2022-12-03 17:00:20 +0530 |
---|---|---|
committer | Indrajith K L | 2022-12-03 17:00:20 +0530 |
commit | f5c4671bfbad96bf346bd7e9a21fc4317b4959df (patch) | |
tree | 2764fc62da58f2ba8da7ed341643fc359873142f /v_windows/v/vlib/term/ui | |
download | cli-tools-windows-master.tar.gz cli-tools-windows-master.tar.bz2 cli-tools-windows-master.zip |
Diffstat (limited to 'v_windows/v/vlib/term/ui')
-rw-r--r-- | v_windows/v/vlib/term/ui/README.md | 99 | ||||
-rw-r--r-- | v_windows/v/vlib/term/ui/color.v | 88 | ||||
-rw-r--r-- | v_windows/v/vlib/term/ui/consoleapi_windows.c.v | 82 | ||||
-rw-r--r-- | v_windows/v/vlib/term/ui/input.v | 241 | ||||
-rw-r--r-- | v_windows/v/vlib/term/ui/input_nix.c.v | 70 | ||||
-rw-r--r-- | v_windows/v/vlib/term/ui/input_windows.c.v | 326 | ||||
-rw-r--r-- | v_windows/v/vlib/term/ui/termios_nix.c.v | 530 | ||||
-rw-r--r-- | v_windows/v/vlib/term/ui/ui.v | 256 |
8 files changed, 1692 insertions, 0 deletions
diff --git a/v_windows/v/vlib/term/ui/README.md b/v_windows/v/vlib/term/ui/README.md new file mode 100644 index 0000000..6bce054 --- /dev/null +++ b/v_windows/v/vlib/term/ui/README.md @@ -0,0 +1,99 @@ +## `term.ui` + +A V module for designing terminal UI apps + +#### Quickstart + +```v +import term.ui as tui + +struct App { +mut: + tui &tui.Context = 0 +} + +fn event(e &tui.Event, x voidptr) { + mut app := &App(x) + println(e) + if e.typ == .key_down && e.code == .escape { + exit(0) + } +} + +fn frame(x voidptr) { + mut app := &App(x) + + app.tui.clear() + app.tui.set_bg_color(r: 63, g: 81, b: 181) + app.tui.draw_rect(20, 6, 41, 10) + app.tui.draw_text(24, 8, 'Hello from V!') + app.tui.set_cursor_position(0, 0) + + app.tui.reset() + app.tui.flush() +} + +mut app := &App{} +app.tui = tui.init( + user_data: app + event_fn: event + frame_fn: frame + hide_cursor: true +) +app.tui.run() ? +``` + +See the `/examples/term.ui/` folder for more usage examples. + +#### Configuration + +- `user_data voidptr` - a pointer to any `user_data`, it will be passed as the last argument to + each callback. Used for accessing your app context from the different callbacks. +- `init_fn fn(voidptr)` - a callback that will be called after initialization + and before the first event / frame. Useful for initializing any user data. +- `frame_fn fn(voidptr)` - a callback that will be fired on each frame, + at a rate of `frame_rate` frames per second. +`event_fn fn(&Event, voidptr)` - a callback that will be fired for every event received. +- `cleanup_fn fn(voidptr)` - a callback that will be fired once, before the application exits. +- `fail_fn fn(string)` - a callback that will be fired + if a fatal error occurs during app initialization. +- `buffer_size int = 256` - the internal size of the read buffer. + Increasing it may help in case you're missing events, but you probably shouldn't lower + this value unless you make sure you're still receiving all events. In general, + higher frame rates work better with lower buffer sizes, and vice versa. +- `frame_rate int = 30` - the number of times per second that the `frame` callback will be fired. + 30fps is a nice balance between smoothness and performance, + but you can increase or lower it as you wish. +- `hide_cursor bool` - whether to hide the mouse cursor. Useful if you want to use your own. +- `capture_events bool` - sets the terminal into raw mode, which makes it intercept some + escape codes such as `ctrl + c` and `ctrl + z`. + Useful if you want to use those key combinations in your app. +- `window_title string` - sets the title of the terminal window. + This may be changed later, by calling the `set_window_title()` method. +- `reset []int = [1, 2, 3, 4, 6, 7, 8, 9, 11, 13, 14, 15, 19]` - a list of reset signals, + to setup handlers to cleanup the terminal state when they're received. + You should not need to change this, unless you know what you're doing. + +All of these fields may be omitted, in which case, the default value will be used. +In the case of the various callbacks, they will not be fired if a handler has not been specified. + + +#### FAQ + +Q: My terminal (doesn't receive events / doesn't print anything / prints gibberish characters), +what's up with that? +A: Please check if your terminal. The module has been tested with `xterm`-based terminals on Linux +(like `gnome-terminal` and `konsole`), and `Terminal.app` and `iterm2` on macOS. +If your terminal does not work, open an issue with the output of `echo $TERM`. + +Q: There are screen tearing issues when doing large prints +A: This is an issue with how terminals render frames, +as they may decide to do so in the middle of receiving a frame, +and cannot be fully fixed unless your console implements the [synchronized updates spec](https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec). +It can be reduced *drastically*, though, by using the rendering methods built in to the module, +and by only painting frames when your app's content has actually changed. + +Q: Why does the module only emit `keydown` events, and not `keyup` like `sokol`/`gg`? +A: It's because of the way terminals emit events. Every key event is received as a keypress, +and there isn't a way of telling terminals to send keyboard events differently, +nor a reliable way of converting these into `keydown` / `keyup` events. diff --git a/v_windows/v/vlib/term/ui/color.v b/v_windows/v/vlib/term/ui/color.v new file mode 100644 index 0000000..3e0a0bb --- /dev/null +++ b/v_windows/v/vlib/term/ui/color.v @@ -0,0 +1,88 @@ +// radare - LGPL - Copyright 2013-2020 - pancake, xarkes +// ansi 256 color extension for r_cons +// https://en.wikipedia.org/wiki/ANSI_color + +module ui + +const ( + value_range = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]! + color_table = init_color_table() +) + +[direct_array_access] +fn init_color_table() []int { + mut color_table_ := []int{len: 256} + // ansi colors + color_table_[0] = 0x000000 + color_table_[1] = 0x800000 + color_table_[2] = 0x008000 + color_table_[3] = 0x808000 + color_table_[4] = 0x000080 + color_table_[5] = 0x800080 + color_table_[6] = 0x008080 + color_table_[7] = 0xc0c0c0 + color_table_[8] = 0x808080 + color_table_[9] = 0xff0000 + color_table_[10] = 0x00ff00 + color_table_[11] = 0xffff00 + color_table_[12] = 0x0000ff + color_table_[13] = 0xff00ff + color_table_[14] = 0x00ffff + color_table_[15] = 0xffffff + // color palette + for i in 0 .. 216 { + r := ui.value_range[(i / 36) % 6] + g := ui.value_range[(i / 6) % 6] + b := ui.value_range[i % 6] + color_table_[i + 16] = ((r << 16) & 0xffffff) + ((g << 8) & 0xffff) + (b & 0xff) + } + // grayscale + for i in 0 .. 24 { + r := 8 + (i * 10) + color_table_[i + 232] = ((r << 16) & 0xffffff) + ((r << 8) & 0xffff) + (r & 0xff) + } + return color_table_ +} + +fn clamp(x int, y int, z int) int { + if x < y { + return y + } + if x > z { + return z + } + return x +} + +fn approximate_rgb(r int, g int, b int) int { + grey := r > 0 && r < 255 && r == g && r == b + if grey { + return 232 + int(f64(r) / (255 / 24.1)) + } + k := int(256.0 / 6) + r2 := clamp(r / k, 0, 5) + g2 := clamp(g / k, 0, 5) + b2 := clamp(b / k, 0, 5) + return 16 + (r2 * 36) + (g2 * 6) + b2 +} + +fn lookup_rgb(r int, g int, b int) int { + color := (r << 16) + (g << 8) + b + // lookup extended colors only, coz non-extended can be changed by users. + for i in 16 .. 256 { + if ui.color_table[i] == color { + return i + } + } + return -1 +} + +// converts an RGB color to an ANSI 256-color, approximating it to the nearest available color +// if an exact match is not found +fn rgb2ansi(r int, g int, b int) int { + c := lookup_rgb(r, g, b) + if c == -1 { + return approximate_rgb(r, g, b) + } + return c +} diff --git a/v_windows/v/vlib/term/ui/consoleapi_windows.c.v b/v_windows/v/vlib/term/ui/consoleapi_windows.c.v new file mode 100644 index 0000000..a6002a6 --- /dev/null +++ b/v_windows/v/vlib/term/ui/consoleapi_windows.c.v @@ -0,0 +1,82 @@ +module ui + +union C.Event { + KeyEvent C.KEY_EVENT_RECORD + MouseEvent C.MOUSE_EVENT_RECORD + WindowBufferSizeEvent C.WINDOW_BUFFER_SIZE_RECORD + MenuEvent C.MENU_EVENT_RECORD + FocusEvent C.FOCUS_EVENT_RECORD +} + +[typedef] +struct C.INPUT_RECORD { + EventType u16 + Event C.Event +} + +union C.uChar { + UnicodeChar rune + AsciiChar byte +} + +[typedef] +struct C.KEY_EVENT_RECORD { + bKeyDown int + wRepeatCount u16 + wVirtualKeyCode u16 + wVirtualScanCode u16 + uChar C.uChar + dwControlKeyState u32 +} + +[typedef] +struct C.MOUSE_EVENT_RECORD { + dwMousePosition C.COORD + dwButtonState u32 + dwControlKeyState u32 + dwEventFlags u32 +} + +[typedef] +struct C.WINDOW_BUFFER_SIZE_RECORD { + dwSize C.COORD +} + +[typedef] +struct C.MENU_EVENT_RECORD { + dwCommandId u32 +} + +[typedef] +struct C.FOCUS_EVENT_RECORD { + bSetFocus int +} + +[typedef] +struct C.COORD { + X i16 + Y i16 +} + +[typedef] +struct C.SMALL_RECT { + Left u16 + Top u16 + Right u16 + Bottom u16 +} + +[typedef] +struct C.CONSOLE_SCREEN_BUFFER_INFO { + dwSize C.COORD + dwCursorPosition C.COORD + wAttributes u16 + srWindow C.SMALL_RECT + dwMaximumWindowSize C.COORD +} + +fn C.ReadConsoleInput(hConsoleInput C.HANDLE, lpBuffer &C.INPUT_RECORD, nLength u32, lpNumberOfEventsRead &u32) bool + +fn C.GetNumberOfConsoleInputEvents(hConsoleInput C.HANDLE, lpcNumberOfEvents &u32) bool + +fn C.GetConsoleScreenBufferInfo(handle C.HANDLE, info &C.CONSOLE_SCREEN_BUFFER_INFO) bool diff --git a/v_windows/v/vlib/term/ui/input.v b/v_windows/v/vlib/term/ui/input.v new file mode 100644 index 0000000..0532b39 --- /dev/null +++ b/v_windows/v/vlib/term/ui/input.v @@ -0,0 +1,241 @@ +// Copyright (c) 2020-2021 Raúl Hernández. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module ui + +import os + +pub enum KeyCode { + null = 0 + tab = 9 + enter = 10 + escape = 27 + space = 32 + backspace = 127 + exclamation = 33 + double_quote = 34 + hashtag = 35 + dollar = 36 + percent = 37 + ampersand = 38 + single_quote = 39 + left_paren = 40 + right_paren = 41 + asterisk = 42 + plus = 43 + comma = 44 + minus = 45 + period = 46 + slash = 47 + _0 = 48 + _1 = 49 + _2 = 50 + _3 = 51 + _4 = 52 + _5 = 53 + _6 = 54 + _7 = 55 + _8 = 56 + _9 = 57 + colon = 58 + semicolon = 59 + less_than = 60 + equal = 61 + greater_than = 62 + question_mark = 63 + at = 64 + a = 97 + b = 98 + c = 99 + d = 100 + e = 101 + f = 102 + g = 103 + h = 104 + i = 105 + j = 106 + k = 107 + l = 108 + m = 109 + n = 110 + o = 111 + p = 112 + q = 113 + r = 114 + s = 115 + t = 116 + u = 117 + v = 118 + w = 119 + x = 120 + y = 121 + z = 122 + left_square_bracket = 91 + backslash = 92 + right_square_bracket = 93 + caret = 94 + underscore = 95 + backtick = 96 + left_curly_bracket = 123 + vertical_bar = 124 + right_curly_bracket = 125 + tilde = 126 + insert = 260 + delete = 261 + up = 262 + down = 263 + right = 264 + left = 265 + page_up = 266 + page_down = 267 + home = 268 + end = 269 + f1 = 290 + f2 = 291 + f3 = 292 + f4 = 293 + f5 = 294 + f6 = 295 + f7 = 296 + f8 = 297 + f9 = 298 + f10 = 299 + f11 = 300 + f12 = 301 + f13 = 302 + f14 = 303 + f15 = 304 + f16 = 305 + f17 = 306 + f18 = 307 + f19 = 308 + f20 = 309 + f21 = 310 + f22 = 311 + f23 = 312 + f24 = 313 +} + +pub enum Direction { + unknown + up + down + left + right +} + +pub enum MouseButton { + unknown + left + middle + right +} + +pub enum EventType { + unknown + mouse_down + mouse_up + mouse_move + mouse_drag + mouse_scroll + key_down + resized +} + +[flag] +pub enum Modifiers { + ctrl + shift + alt +} + +pub struct Event { +pub: + typ EventType + // Mouse event info + x int + y int + button MouseButton + direction Direction + // Keyboard event info + code KeyCode + modifiers Modifiers + ascii byte + utf8 string + // Resized event info + width int + height int +} + +pub struct Context { + ExtraContext // contains fields specific to an implementation +pub: + cfg Config // adsasdas +mut: + print_buf []byte + paused bool + enable_su bool + enable_rgb bool +pub mut: + frame_count u64 + window_width int + window_height int +} + +pub struct Config { + user_data voidptr + init_fn fn (voidptr) + frame_fn fn (voidptr) + cleanup_fn fn (voidptr) + event_fn fn (&Event, voidptr) + fail_fn fn (string) + + buffer_size int = 256 + frame_rate int = 30 + use_x11 bool + + window_title string + hide_cursor bool + capture_events bool + use_alternate_buffer bool = true + skip_init_checks bool + // All kill signals to set up exit listeners on: + reset []os.Signal = [.hup, .int, .quit, .ill, .abrt, .bus, .fpe, .kill, .segv, .pipe, .alrm, .term, + .stop, +] +} + +[inline] +fn (ctx &Context) init() { + if ctx.cfg.init_fn != voidptr(0) { + ctx.cfg.init_fn(ctx.cfg.user_data) + } +} + +[inline] +fn (ctx &Context) frame() { + if ctx.cfg.frame_fn != voidptr(0) { + ctx.cfg.frame_fn(ctx.cfg.user_data) + } +} + +[inline] +fn (ctx &Context) cleanup() { + if ctx.cfg.cleanup_fn != voidptr(0) { + ctx.cfg.cleanup_fn(ctx.cfg.user_data) + } +} + +[inline] +fn (ctx &Context) fail(error string) { + if ctx.cfg.fail_fn != voidptr(0) { + ctx.cfg.fail_fn(error) + } +} + +[inline] +fn (ctx &Context) event(event &Event) { + if ctx.cfg.event_fn != voidptr(0) { + ctx.cfg.event_fn(event, ctx.cfg.user_data) + } +} diff --git a/v_windows/v/vlib/term/ui/input_nix.c.v b/v_windows/v/vlib/term/ui/input_nix.c.v new file mode 100644 index 0000000..e806fb8 --- /dev/null +++ b/v_windows/v/vlib/term/ui/input_nix.c.v @@ -0,0 +1,70 @@ +// Copyright (c) 2020-2021 Raúl Hernández. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module ui + +struct ExtraContext { +mut: + read_buf []byte +} + +const ( + ctx_ptr = &Context(0) +) + +pub fn init(cfg Config) &Context { + mut ctx := &Context{ + cfg: cfg + } + ctx.read_buf = []byte{cap: cfg.buffer_size} + + // lmao + unsafe { + x := &ui.ctx_ptr + *x = ctx + _ = x + } + return ctx +} + +[inline] +fn save_title() { + // restore the previously saved terminal title + print('\x1b[22;0t') +} + +[inline] +fn load_title() { + // restore the previously saved terminal title + print('\x1b[23;0t') +} + +pub fn (mut ctx Context) run() ? { + if ctx.cfg.use_x11 { + ctx.fail('error: x11 backend not implemented yet') + exit(1) + } else { + ctx.termios_setup() ? + ctx.termios_loop() + } +} + +// shifts the array left, to remove any data that was just read, and updates its len +// TODO: remove +[inline] +fn (mut ctx Context) shift(len int) { + unsafe { + C.memmove(ctx.read_buf.data, &byte(ctx.read_buf.data) + len, ctx.read_buf.cap - len) + ctx.resize_arr(ctx.read_buf.len - len) + } +} + +// TODO: don't actually do this, lmao +[inline] +fn (mut ctx Context) resize_arr(size int) { + mut l := unsafe { &ctx.read_buf.len } + unsafe { + *l = size + _ = l + } +} diff --git a/v_windows/v/vlib/term/ui/input_windows.c.v b/v_windows/v/vlib/term/ui/input_windows.c.v new file mode 100644 index 0000000..bd9782d --- /dev/null +++ b/v_windows/v/vlib/term/ui/input_windows.c.v @@ -0,0 +1,326 @@ +// Copyright (c) 2020-2021 Raúl Hernández. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module ui + +import os +import time + +const ( + buf_size = 64 + ctx_ptr = &Context(0) + stdin_at_startup = u32(0) +) + +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() { + if ui.ctx_ptr != 0 { + if ui.ctx_ptr.cfg.use_alternate_buffer { + // clear the terminal and set the cursor to the origin + print('\x1b[2J\x1b[3J') + print('\x1b[?1049l') + } + C.SetConsoleMode(ui.ctx_ptr.stdin_handle, ui.stdin_at_startup) + } + load_title() + os.flush() +} + +pub fn init(cfg Config) &Context { + 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 + if C.GetConsoleMode(stdin_handle, &ui.stdin_at_startup) == 0 { + 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') + } + + if ctx.cfg.hide_cursor { + ctx.hide_cursor() + ctx.flush() + } + + if ctx.cfg.window_title != '' { + print('\x1b]0;$ctx.cfg.window_title\x07') + } + + unsafe { + x := &ui.ctx_ptr + *x = ctx + } + C.atexit(restore_terminal_state) + for code in ctx.cfg.reset { + os.signal_opt(code, fn (_ os.Signal) { + mut c := ui.ctx_ptr + if c != 0 { + c.cleanup() + } + exit(0) + }) or {} + } + + ctx.stdin_handle = stdin_handle + ctx.stdout_handle = stdout_handle + return ctx +} + +pub fn (mut ctx Context) run() ? { + 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 { + time.sleep(sleep_len * time.microsecond) + } + 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') + } + if nr_events < 1 { + return + } + + // print('$nr_events | ') + if !C.ReadConsoleInput(ctx.stdin_handle, &ctx.read_buf[0], ui.buf_size, &nr_events) { + 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 } + if e.bKeyDown == 0 { + continue + } + // we don't handle key_up events because they don't exist on linux... + // 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 } + 14...20 { KeyCode.null } + 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 } + 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 + else { KeyCode(ascii) } + } + + mut modifiers := Modifiers{} + 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) + } + + 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') + } + x := e.dwMousePosition.X + 1 + y := int(e.dwMousePosition.Y) - sb_info.srWindow.Top + 1 + mut modifiers := Modifiers{} + 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) + } + // 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 + }) + } + C.MOUSE_WHEELED { + ctx.event(&Event{ + typ: .mouse_scroll + direction: if i16(e.dwButtonState >> 16) < 0 { + Direction.up + } else { + Direction.down + } + x: x + y: y + modifiers: modifiers + }) + } + 0x0008 /* C.MOUSE_HWHEELED */ { + ctx.event(&Event{ + typ: .mouse_scroll + direction: if i16(e.dwButtonState >> 16) < 0 { + Direction.right + } else { + Direction.left + } + x: x + y: y + modifiers: modifiers + }) + } + 0 /* CLICK */, C.DOUBLE_CLICK { + 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 + }) + } + else {} + } + } + 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') + } + w := sb.srWindow.Right - sb.srWindow.Left + 1 + h := sb.srWindow.Bottom - sb.srWindow.Top + 1 + 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 {} + } + } +} + +[inline] +fn save_title() { + // restore the previously saved terminal title + print('\x1b[22;0t') +} + +[inline] +fn load_title() { + // restore the previously saved terminal title + print('\x1b[23;0t') +} diff --git a/v_windows/v/vlib/term/ui/termios_nix.c.v b/v_windows/v/vlib/term/ui/termios_nix.c.v new file mode 100644 index 0000000..fb5ff76 --- /dev/null +++ b/v_windows/v/vlib/term/ui/termios_nix.c.v @@ -0,0 +1,530 @@ +// Copyright (c) 2020-2021 Raúl Hernández. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module ui + +import os +import time + +#include <termios.h> +#include <sys/ioctl.h> +#include <signal.h> + +fn C.tcgetattr(fd int, termios_p &C.termios) int + +fn C.tcsetattr(fd int, optional_actions int, termios_p &C.termios) int + +fn C.ioctl(fd int, request u64, arg voidptr) int + +struct C.termios { +mut: + c_iflag u32 + c_lflag u32 + c_cc [32]byte +} + +struct C.winsize { + ws_row u16 + ws_col u16 +} + +const ( + termios_at_startup = get_termios() +) + +[inline] +fn get_termios() C.termios { + mut t := C.termios{} + C.tcgetattr(C.STDIN_FILENO, &t) + return t +} + +[inline] +fn get_terminal_size() (u16, u16) { + winsz := C.winsize{} + C.ioctl(0, C.TIOCGWINSZ, &winsz) + return winsz.ws_row, winsz.ws_col +} + +fn restore_terminal_state_signal(_ os.Signal) { + restore_terminal_state() +} + +fn restore_terminal_state() { + termios_reset() + mut c := ctx_ptr + if c != 0 { + c.paused = true + load_title() + } + os.flush() +} + +fn (mut ctx Context) termios_setup() ? { + // store the current title, so restore_terminal_state can get it back + save_title() + + if !ctx.cfg.skip_init_checks && !(os.is_atty(C.STDIN_FILENO) != 0 + && os.is_atty(C.STDOUT_FILENO) != 0) { + return error('not running under a TTY') + } + + mut termios := get_termios() + + if ctx.cfg.capture_events { + // Set raw input mode by unsetting ICANON and ECHO, + // as well as disable e.g. ctrl+c and ctrl.z + termios.c_iflag &= ~u32(C.IGNBRK | C.BRKINT | C.PARMRK | C.IXON) + termios.c_lflag &= ~u32(C.ICANON | C.ISIG | C.ECHO | C.IEXTEN | C.TOSTOP) + } else { + // Set raw input mode by unsetting ICANON and ECHO + termios.c_lflag &= ~u32(C.ICANON | C.ECHO) + } + + if ctx.cfg.hide_cursor { + ctx.hide_cursor() + ctx.flush() + } + + if ctx.cfg.window_title != '' { + print('\x1b]0;$ctx.cfg.window_title\x07') + } + + if !ctx.cfg.skip_init_checks { + // prevent blocking during the feature detections, but allow enough time for the terminal + // to send back the relevant input data + termios.c_cc[C.VTIME] = 1 + termios.c_cc[C.VMIN] = 0 + C.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, &termios) + // feature-test the SU spec + sx, sy := get_cursor_position() + print('$bsu$esu') + ex, ey := get_cursor_position() + if sx == ex && sy == ey { + // the terminal either ignored or handled the sequence properly, enable SU + ctx.enable_su = true + } else { + ctx.draw_line(sx, sy, ex, ey) + ctx.set_cursor_position(sx, sy) + ctx.flush() + } + // feature-test rgb (truecolor) support + ctx.enable_rgb = supports_truecolor() + } + // Prevent stdin from blocking by making its read time 0 + termios.c_cc[C.VTIME] = 0 + termios.c_cc[C.VMIN] = 0 + C.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, &termios) + // enable mouse input + print('\x1b[?1003h\x1b[?1006h') + 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') + } + ctx.window_height, ctx.window_width = get_terminal_size() + + // Reset console on exit + C.atexit(restore_terminal_state) + os.signal_opt(.tstp, restore_terminal_state_signal) or {} + os.signal_opt(.cont, fn (_ os.Signal) { + mut c := ctx_ptr + if c != 0 { + c.termios_setup() or { panic(err) } + c.window_height, c.window_width = get_terminal_size() + mut event := &Event{ + typ: .resized + width: c.window_width + height: c.window_height + } + c.paused = false + c.event(event) + } + }) or {} + for code in ctx.cfg.reset { + os.signal_opt(code, fn (_ os.Signal) { + mut c := ctx_ptr + if c != 0 { + c.cleanup() + } + exit(0) + }) or {} + } + + os.signal_opt(.winch, fn (_ os.Signal) { + mut c := ctx_ptr + if c != 0 { + c.window_height, c.window_width = get_terminal_size() + + mut event := &Event{ + typ: .resized + width: c.window_width + height: c.window_height + } + c.event(event) + } + }) or {} + + os.flush() +} + +fn get_cursor_position() (int, int) { + print('\033[6n') + mut s := '' + unsafe { + buf := malloc_noscan(25) + len := C.read(C.STDIN_FILENO, buf, 24) + buf[len] = 0 + s = tos(buf, len) + } + a := s[2..].split(';') + if a.len != 2 { + return -1, -1 + } + return a[0].int(), a[1].int() +} + +fn supports_truecolor() bool { + // faster/simpler, but less reliable, check + if os.getenv('COLORTERM') in ['truecolor', '24bit'] { + return true + } + // set the bg color to some arbirtrary value (#010203), assumed not to be the default + print('\x1b[48:2:1:2:3m') + // andquery the current color + print('\x1bP\$qm\x1b\\') + mut s := '' + unsafe { + buf := malloc_noscan(25) + len := C.read(C.STDIN_FILENO, buf, 24) + buf[len] = 0 + s = tos(buf, len) + } + return s.contains('1:2:3') +} + +fn termios_reset() { + // C.TCSANOW ?? + C.tcsetattr(C.STDIN_FILENO, C.TCSAFLUSH, &ui.termios_at_startup) + print('\x1b[?1003l\x1b[?1006l\x1b[?25h') + c := ctx_ptr + if c != 0 && c.cfg.use_alternate_buffer { + print('\x1b[?1049l') + } + os.flush() +} + +/////////////////////////////////////////// +// TODO: do multiple sleep/read cycles, rather than one big one +fn (mut ctx Context) termios_loop() { + 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 + } + // println('SLEEPING: $sleep_len') + if sleep_len > 0 { + time.sleep(sleep_len * time.microsecond) + } + if !ctx.paused { + sw.restart() + if ctx.cfg.event_fn != voidptr(0) { + unsafe { + len := C.read(C.STDIN_FILENO, &byte(ctx.read_buf.data) + ctx.read_buf.len, + ctx.read_buf.cap - ctx.read_buf.len) + ctx.resize_arr(ctx.read_buf.len + len) + } + if ctx.read_buf.len > 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() { + // Stop this from getting stuck in rare cases where something isn't parsed correctly + mut nr_iters := 0 + for ctx.read_buf.len > 0 { + nr_iters++ + if nr_iters > 100 { + ctx.shift(1) + } + mut event := &Event(0) + if ctx.read_buf[0] == 0x1b { + e, len := escape_sequence(ctx.read_buf.bytestr()) + event = e + ctx.shift(len) + } else { + event = single_char(ctx.read_buf.bytestr()) + ctx.shift(1) + } + if event != 0 { + ctx.event(event) + nr_iters = 0 + } + } +} + +fn single_char(buf string) &Event { + ch := buf[0] + + mut event := &Event{ + typ: .key_down + ascii: ch + code: KeyCode(ch) + utf8: buf + } + + match ch { + // special handling for `ctrl + letter` + // TODO: Fix assoc in V and remove this workaround :/ + // 1 ... 26 { event = Event{ ...event, code: KeyCode(96 | ch), modifiers: .ctrl } } + // 65 ... 90 { event = Event{ ...event, code: KeyCode(32 | ch), modifiers: .shift } } + // The bit `or`s here are really just `+`'s, just written in this way for a tiny performance improvement + // don't treat tab, enter as ctrl+i, ctrl+j + 1...8, 11...26 { + event = &Event{ + typ: event.typ + ascii: event.ascii + utf8: event.utf8 + code: KeyCode(96 | ch) + modifiers: .ctrl + } + } + 65...90 { + event = &Event{ + typ: event.typ + ascii: event.ascii + utf8: event.utf8 + code: KeyCode(32 | ch) + modifiers: .shift + } + } + else {} + } + + return event +} + +// Gets an entire, independent escape sequence from the buffer +// Normally, this just means reading until the first letter, but there are some exceptions... +fn escape_end(buf string) int { + mut i := 0 + for { + if i + 1 == buf.len { + return buf.len + } + + if buf[i].is_letter() || buf[i] == `~` { + if buf[i] == `O` && i + 2 <= buf.len { + n := buf[i + 1] + if (n >= `A` && n <= `D`) || (n >= `P` && n <= `S`) || n == `F` || n == `H` { + return i + 2 + } + } + return i + 1 + // escape hatch to avoid potential issues/crashes, although ideally this should never eval to true + } else if buf[i + 1] == 0x1b { + return i + 1 + } + i++ + } + // this point should be unreachable + assert false + return 0 +} + +fn escape_sequence(buf_ string) (&Event, int) { + end := escape_end(buf_) + single := buf_[..end] // read until the end of the sequence + buf := single[1..] // skip the escape character + + if buf.len == 0 { + return &Event{ + typ: .key_down + ascii: 27 + code: .escape + utf8: single + }, 1 + } + + if buf.len == 1 { + c := single_char(buf) + mut modifiers := c.modifiers + modifiers.set(.alt) + return &Event{ + typ: c.typ + ascii: c.ascii + code: c.code + utf8: single + modifiers: modifiers + }, 2 + } + // ---------------- + // Mouse events + // ---------------- + // Documentation: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking + if buf.len > 2 && buf[1] == `<` { + split := buf[2..].split(';') + if split.len < 3 { + return &Event(0), 0 + } + + typ, x, y := split[0].int(), split[1].int(), split[2].int() + lo := typ & 0b00011 + hi := typ & 0b11100 + + mut modifiers := Modifiers{} + if hi & 4 != 0 { + modifiers.set(.shift) + } + if hi & 8 != 0 { + modifiers.set(.alt) + } + if hi & 16 != 0 { + modifiers.set(.ctrl) + } + + match typ { + 0...31 { + last := buf[buf.len - 1] + button := if lo < 3 { MouseButton(lo + 1) } else { MouseButton.unknown } + event := if last == `m` || lo == 3 { + EventType.mouse_up + } else { + EventType.mouse_down + } + + return &Event{ + typ: event + x: x + y: y + button: button + modifiers: modifiers + utf8: single + }, end + } + 32...63 { + button, event := if lo < 3 { + MouseButton(lo + 1), EventType.mouse_drag + } else { + MouseButton.unknown, EventType.mouse_move + } + + return &Event{ + typ: event + x: x + y: y + button: button + modifiers: modifiers + utf8: single + }, end + } + 64...95 { + direction := if typ & 1 == 0 { Direction.down } else { Direction.up } + return &Event{ + typ: .mouse_scroll + x: x + y: y + direction: direction + modifiers: modifiers + utf8: single + }, end + } + else { + return &Event{ + typ: .unknown + utf8: single + }, end + } + } + } + // ---------------------------- + // Special key combinations + // ---------------------------- + + mut code := KeyCode.null + mut modifiers := Modifiers{} + match buf { + '[A', 'OA' { code = .up } + '[B', 'OB' { code = .down } + '[C', 'OC' { code = .right } + '[D', 'OD' { code = .left } + '[5~', '[[5~' { code = .page_up } + '[6~', '[[6~' { code = .page_down } + '[F', 'OF', '[4~', '[[8~' { code = .end } + '[H', 'OH', '[1~', '[[7~' { code = .home } + '[2~' { code = .insert } + '[3~' { code = .delete } + 'OP', '[11~' { code = .f1 } + 'OQ', '[12~' { code = .f2 } + 'OR', '[13~' { code = .f3 } + 'OS', '[14~' { code = .f4 } + '[15~' { code = .f5 } + '[17~' { code = .f6 } + '[18~' { code = .f7 } + '[19~' { code = .f8 } + '[20~' { code = .f9 } + '[21~' { code = .f10 } + '[23~' { code = .f11 } + '[24~' { code = .f12 } + else {} + } + + if buf == '[Z' { + code = .tab + modifiers.set(.shift) + } + + if buf.len == 5 && buf[0] == `[` && buf[1].is_digit() && buf[2] == `;` { + match buf[3] { + `2` { modifiers = .shift } + `3` { modifiers = .alt } + `4` { modifiers = .shift | .alt } + `5` { modifiers = .ctrl } + `6` { modifiers = .ctrl | .shift } + `7` { modifiers = .ctrl | .alt } + `8` { modifiers = .ctrl | .alt | .shift } + else {} + } + + if buf[1] == `1` { + match buf[4] { + `A` { code = KeyCode.up } + `B` { code = KeyCode.down } + `C` { code = KeyCode.right } + `D` { code = KeyCode.left } + `F` { code = KeyCode.end } + `H` { code = KeyCode.home } + `P` { code = KeyCode.f1 } + `Q` { code = KeyCode.f2 } + `R` { code = KeyCode.f3 } + `S` { code = KeyCode.f4 } + else {} + } + } else if buf[1] == `5` { + code = KeyCode.page_up + } else if buf[1] == `6` { + code = KeyCode.page_down + } + } + + return &Event{ + typ: .key_down + code: code + utf8: single + modifiers: modifiers + }, end +} diff --git a/v_windows/v/vlib/term/ui/ui.v b/v_windows/v/vlib/term/ui/ui.v new file mode 100644 index 0000000..6ba3d7c --- /dev/null +++ b/v_windows/v/vlib/term/ui/ui.v @@ -0,0 +1,256 @@ +// Copyright (c) 2020-2021 Raúl Hernández. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module ui + +import strings + +pub struct Color { +pub: + r byte + g byte + b byte +} + +pub fn (c Color) hex() string { + return '#$c.r.hex()$c.g.hex()$c.b.hex()' +} + +// Synchronized Updates spec, designed to avoid tearing during renders +// https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec +const ( + bsu = '\x1bP=1s\x1b\\' + esu = '\x1bP=2s\x1b\\' +) + +// write puts the string `s` into the print buffer. +[inline] +pub fn (mut ctx Context) write(s string) { + if s == '' { + return + } + unsafe { ctx.print_buf.push_many(s.str, s.len) } +} + +// flush displays the accumulated print buffer to the screen. +[inline] +pub fn (mut ctx Context) flush() { + // TODO: Diff the previous frame against this one, and only render things that changed? + if !ctx.enable_su { + C.write(1, ctx.print_buf.data, ctx.print_buf.len) + } else { + C.write(1, ui.bsu.str, ui.bsu.len) + C.write(1, ctx.print_buf.data, ctx.print_buf.len) + C.write(1, ui.esu.str, ui.esu.len) + } + ctx.print_buf.clear() +} + +// bold sets the character state to bold. +[inline] +pub fn (mut ctx Context) bold() { + ctx.write('\x1b[1m') +} + +// set_cursor_position positions the cusor at the given coordinates `x`,`y`. +[inline] +pub fn (mut ctx Context) set_cursor_position(x int, y int) { + ctx.write('\x1b[$y;${x}H') +} + +// show_cursor will make the cursor appear if it is not already visible +[inline] +pub fn (mut ctx Context) show_cursor() { + ctx.write('\x1b[?25h') +} + +// hide_cursor will make the cursor invisible +[inline] +pub fn (mut ctx Context) hide_cursor() { + ctx.write('\x1b[?25l') +} + +// set_color sets the current foreground color used by any succeeding `draw_*` calls. +[inline] +pub fn (mut ctx Context) set_color(c Color) { + if ctx.enable_rgb { + ctx.write('\x1b[38;2;${int(c.r)};${int(c.g)};${int(c.b)}m') + } else { + ctx.write('\x1b[38;5;${rgb2ansi(c.r, c.g, c.b)}m') + } +} + +// set_color sets the current background color used by any succeeding `draw_*` calls. +[inline] +pub fn (mut ctx Context) set_bg_color(c Color) { + if ctx.enable_rgb { + ctx.write('\x1b[48;2;${int(c.r)};${int(c.g)};${int(c.b)}m') + } else { + ctx.write('\x1b[48;5;${rgb2ansi(c.r, c.g, c.b)}m') + } +} + +// reset_color sets the current foreground color back to it's default value. +[inline] +pub fn (mut ctx Context) reset_color() { + ctx.write('\x1b[39m') +} + +// reset_bg_color sets the current background color back to it's default value. +[inline] +pub fn (mut ctx Context) reset_bg_color() { + ctx.write('\x1b[49m') +} + +// reset restores the state of all colors and text formats back to their default values. +[inline] +pub fn (mut ctx Context) reset() { + ctx.write('\x1b[0m') +} + +[inline] +pub fn (mut ctx Context) clear() { + ctx.write('\x1b[2J\x1b[3J') +} + +// set_window_title sets the string `s` as the window title. +[inline] +pub fn (mut ctx Context) set_window_title(s string) { + print('\x1b]0;$s\x07') +} + +// draw_point draws a point at position `x`,`y`. +[inline] +pub fn (mut ctx Context) draw_point(x int, y int) { + ctx.set_cursor_position(x, y) + ctx.write(' ') +} + +// draw_text draws the string `s`, starting from position `x`,`y`. +[inline] +pub fn (mut ctx Context) draw_text(x int, y int, s string) { + ctx.set_cursor_position(x, y) + ctx.write(s) +} + +// draw_line draws a line segment, starting at point `x`,`y`, and ending at point `x2`,`y2`. +pub fn (mut ctx Context) draw_line(x int, y int, x2 int, y2 int) { + min_x, min_y := if x < x2 { x } else { x2 }, if y < y2 { y } else { y2 } + max_x, _ := if x > x2 { x } else { x2 }, if y > y2 { y } else { y2 } + if y == y2 { + // Horizontal line, performance improvement + ctx.set_cursor_position(min_x, min_y) + ctx.write(strings.repeat(` `, max_x + 1 - min_x)) + return + } + // Draw the various points with Bresenham's line algorithm: + mut x0, x1 := x, x2 + mut y0, y1 := y, y2 + sx := if x0 < x1 { 1 } else { -1 } + sy := if y0 < y1 { 1 } else { -1 } + dx := if x0 < x1 { x1 - x0 } else { x0 - x1 } + dy := if y0 < y1 { y0 - y1 } else { y1 - y0 } // reversed + mut err := dx + dy + for { + // res << Segment{ x0, y0 } + ctx.draw_point(x0, y0) + if x0 == x1 && y0 == y1 { + break + } + e2 := 2 * err + if e2 >= dy { + err += dy + x0 += sx + } + if e2 <= dx { + err += dx + y0 += sy + } + } +} + +// draw_dashed_line draws a dashed line segment, starting at point `x`,`y`, and ending at point `x2`,`y2`. +pub fn (mut ctx Context) draw_dashed_line(x int, y int, x2 int, y2 int) { + // Draw the various points with Bresenham's line algorithm: + mut x0, x1 := x, x2 + mut y0, y1 := y, y2 + sx := if x0 < x1 { 1 } else { -1 } + sy := if y0 < y1 { 1 } else { -1 } + dx := if x0 < x1 { x1 - x0 } else { x0 - x1 } + dy := if y0 < y1 { y0 - y1 } else { y1 - y0 } // reversed + mut err := dx + dy + mut i := 0 + for { + if i % 2 == 0 { + ctx.draw_point(x0, y0) + } + if x0 == x1 && y0 == y1 { + break + } + e2 := 2 * err + if e2 >= dy { + err += dy + x0 += sx + } + if e2 <= dx { + err += dx + y0 += sy + } + i++ + } +} + +// draw_rect draws a rectangle, starting at top left `x`,`y`, and ending at bottom right `x2`,`y2`. +pub fn (mut ctx Context) draw_rect(x int, y int, x2 int, y2 int) { + if y == y2 || x == x2 { + ctx.draw_line(x, y, x2, y2) + return + } + min_y, max_y := if y < y2 { y, y2 } else { y2, y } + for y_pos in min_y .. max_y + 1 { + ctx.draw_line(x, y_pos, x2, y_pos) + } +} + +// draw_empty_dashed_rect draws a rectangle with dashed lines, starting at top left `x`,`y`, and ending at bottom right `x2`,`y2`. +pub fn (mut ctx Context) draw_empty_dashed_rect(x int, y int, x2 int, y2 int) { + if y == y2 || x == x2 { + ctx.draw_dashed_line(x, y, x2, y2) + return + } + + min_x, max_x := if x < x2 { x, x2 } else { x2, x } + min_y, max_y := if y < y2 { y, y2 } else { y2, y } + + ctx.draw_dashed_line(min_x, min_y, max_x, min_y) + ctx.draw_dashed_line(min_x, min_y, min_x, max_y) + if (max_y - min_y) & 1 == 0 { + ctx.draw_dashed_line(min_x, max_y, max_x, max_y) + } else { + ctx.draw_dashed_line(min_x + 1, max_y, max_x, max_y) + } + if (max_x - min_x) & 1 == 0 { + ctx.draw_dashed_line(max_x, min_y, max_x, max_y) + } else { + ctx.draw_dashed_line(max_x, min_y + 1, max_x, max_y) + } +} + +// draw_empty_rect draws a rectangle with no fill, starting at top left `x`,`y`, and ending at bottom right `x2`,`y2`. +pub fn (mut ctx Context) draw_empty_rect(x int, y int, x2 int, y2 int) { + if y == y2 || x == x2 { + ctx.draw_line(x, y, x2, y2) + return + } + ctx.draw_line(x, y, x2, y) + ctx.draw_line(x, y2, x2, y2) + ctx.draw_line(x, y, x, y2) + ctx.draw_line(x2, y, x2, y2) +} + +// horizontal_separator draws a horizontal separator, spanning the width of the screen. +[inline] +pub fn (mut ctx Context) horizontal_separator(y int) { + ctx.set_cursor_position(0, y) + ctx.write(strings.repeat(`-`, ctx.window_width)) // /* `⎽` */ +} |