aboutsummaryrefslogtreecommitdiff
path: root/v_windows/v/examples/term.ui
diff options
context:
space:
mode:
authorIndrajith K L2022-12-03 17:00:20 +0530
committerIndrajith K L2022-12-03 17:00:20 +0530
commitf5c4671bfbad96bf346bd7e9a21fc4317b4959df (patch)
tree2764fc62da58f2ba8da7ed341643fc359873142f /v_windows/v/examples/term.ui
downloadcli-tools-windows-master.tar.gz
cli-tools-windows-master.tar.bz2
cli-tools-windows-master.zip
Adds most of the toolsHEADmaster
Diffstat (limited to 'v_windows/v/examples/term.ui')
-rw-r--r--v_windows/v/examples/term.ui/README.md1
-rw-r--r--v_windows/v/examples/term.ui/cursor_chaser.v100
-rw-r--r--v_windows/v/examples/term.ui/event_viewer.v47
-rw-r--r--v_windows/v/examples/term.ui/pong.v499
-rw-r--r--v_windows/v/examples/term.ui/rectangles.v97
-rw-r--r--v_windows/v/examples/term.ui/screenshot_pong.pngbin0 -> 1054 bytes
-rw-r--r--v_windows/v/examples/term.ui/term_drawing.v510
-rw-r--r--v_windows/v/examples/term.ui/text_editor.v583
-rw-r--r--v_windows/v/examples/term.ui/vyper.v475
9 files changed, 2312 insertions, 0 deletions
diff --git a/v_windows/v/examples/term.ui/README.md b/v_windows/v/examples/term.ui/README.md
new file mode 100644
index 0000000..409a55e
--- /dev/null
+++ b/v_windows/v/examples/term.ui/README.md
@@ -0,0 +1 @@
+<img src='https://raw.githubusercontent.com/vlang/v/master/examples/term.ui/screenshot_pong.png' width=300>
diff --git a/v_windows/v/examples/term.ui/cursor_chaser.v b/v_windows/v/examples/term.ui/cursor_chaser.v
new file mode 100644
index 0000000..dd987e6
--- /dev/null
+++ b/v_windows/v/examples/term.ui/cursor_chaser.v
@@ -0,0 +1,100 @@
+import term.ui as tui
+
+const (
+ colors = [
+ tui.Color{33, 150, 243},
+ tui.Color{0, 150, 136},
+ tui.Color{205, 220, 57},
+ tui.Color{255, 152, 0},
+ tui.Color{244, 67, 54},
+ tui.Color{156, 39, 176},
+ ]
+)
+
+struct Point {
+ x int
+ y int
+}
+
+struct App {
+mut:
+ tui &tui.Context = 0
+ points []Point
+ color tui.Color = colors[0]
+ color_idx int
+ cut_rate f64 = 5
+}
+
+fn frame(x voidptr) {
+ mut app := &App(x)
+
+ app.tui.clear()
+
+ if app.points.len > 0 {
+ app.tui.set_bg_color(app.color)
+ mut last := app.points[0]
+ for point in app.points {
+ // if the cursor moves quickly enough, different events are not
+ // necessarily touching, so we need to draw a line between them
+ app.tui.draw_line(last.x, last.y, point.x, point.y)
+ last = point
+ }
+ app.tui.reset()
+
+ l := int(app.points.len / app.cut_rate) + 1
+ app.points = app.points[l..].clone()
+ }
+
+ ww := app.tui.window_width
+
+ app.tui.bold()
+ app.tui.draw_text(ww / 6, 2, 'V term.input: cursor chaser demo')
+ app.tui.draw_text((ww - ww / 6) - 14, 2, 'cut rate: ${(100 / app.cut_rate):3.0f}%')
+ app.tui.horizontal_separator(3)
+ app.tui.reset()
+ app.tui.flush()
+}
+
+fn event(e &tui.Event, x voidptr) {
+ mut app := &App(x)
+
+ match e.typ {
+ .key_down {
+ match e.code {
+ .escape {
+ exit(0)
+ }
+ .space, .enter {
+ app.color_idx++
+ if app.color_idx == colors.len {
+ app.color_idx = 0
+ }
+ app.color = colors[app.color_idx]
+ }
+ else {}
+ }
+ }
+ .mouse_move, .mouse_drag, .mouse_down {
+ app.points << Point{e.x, e.y}
+ }
+ .mouse_scroll {
+ d := if e.direction == .up { 0.1 } else { -0.1 }
+ app.cut_rate += d
+ if app.cut_rate < 1 {
+ app.cut_rate = 1
+ }
+ }
+ else {}
+ }
+}
+
+fn main() {
+ mut app := &App{}
+ app.tui = tui.init(
+ user_data: app
+ frame_fn: frame
+ event_fn: event
+ hide_cursor: true
+ )
+ app.tui.run() ?
+}
diff --git a/v_windows/v/examples/term.ui/event_viewer.v b/v_windows/v/examples/term.ui/event_viewer.v
new file mode 100644
index 0000000..f9c443f
--- /dev/null
+++ b/v_windows/v/examples/term.ui/event_viewer.v
@@ -0,0 +1,47 @@
+import term.ui as tui
+
+struct App {
+mut:
+ tui &tui.Context = 0
+}
+
+fn event(e &tui.Event, x voidptr) {
+ mut app := &App(x)
+ app.tui.clear()
+ app.tui.set_cursor_position(0, 0)
+ app.tui.write('V term.input event viewer (press `esc` to exit)\n\n')
+ app.tui.write('$e')
+ app.tui.write('\n\nRaw event bytes: "$e.utf8.bytes().hex()" = $e.utf8.bytes()')
+ if !e.modifiers.is_empty() {
+ app.tui.write('\nModifiers: $e.modifiers = ')
+ if e.modifiers.has(.ctrl) {
+ app.tui.write('ctrl. ')
+ }
+ if e.modifiers.has(.shift) {
+ app.tui.write('shift ')
+ }
+ if e.modifiers.has(.alt) {
+ app.tui.write('alt. ')
+ }
+ }
+ app.tui.flush()
+
+ if e.typ == .key_down && e.code == .escape {
+ exit(0)
+ }
+}
+
+fn main() {
+ mut app := &App{}
+ app.tui = tui.init(
+ user_data: app
+ event_fn: event
+ window_title: 'V term.ui event viewer'
+ hide_cursor: true
+ capture_events: true
+ frame_rate: 60
+ use_alternate_buffer: false
+ )
+ println('V term.ui event viewer (press `esc` to exit)\n\n')
+ app.tui.run() ?
+}
diff --git a/v_windows/v/examples/term.ui/pong.v b/v_windows/v/examples/term.ui/pong.v
new file mode 100644
index 0000000..375261f
--- /dev/null
+++ b/v_windows/v/examples/term.ui/pong.v
@@ -0,0 +1,499 @@
+// Copyright (c) 2020 Lars Pontoppidan. All rights reserved.
+// Use of this source code is governed by the MIT license distributed with this software.
+import term
+import term.ui
+import time
+
+enum Mode {
+ menu
+ game
+}
+
+const (
+ player_one = 1 // Human control this racket
+ player_two = 0 // Take over this AI controller
+ white = ui.Color{255, 255, 255}
+ orange = ui.Color{255, 140, 0}
+)
+
+[heap]
+struct App {
+mut:
+ tui &ui.Context = 0
+ mode Mode = Mode.menu
+ width int
+ height int
+ game &Game = 0
+ dt f32
+ ticks i64
+}
+
+fn (mut a App) init() {
+ a.game = &Game{
+ app: a
+ }
+ w, h := a.tui.window_width, a.tui.window_height
+ a.width = w
+ a.height = h
+ term.erase_del_clear()
+ term.set_cursor_position(
+ x: 0
+ y: 0
+ )
+}
+
+fn (mut a App) start_game() {
+ if a.mode != .game {
+ a.mode = .game
+ }
+ a.game.init()
+}
+
+fn (mut a App) frame() {
+ ticks := time.ticks()
+ a.dt = f32(ticks - a.ticks) / 1000.0
+ a.width, a.height = a.tui.window_width, a.tui.window_height
+ if a.mode == .game {
+ a.game.update()
+ }
+ a.tui.clear()
+ a.render()
+ a.tui.flush()
+ a.ticks = ticks
+}
+
+fn (mut a App) quit() {
+ if a.mode != .menu {
+ a.game.quit()
+ return
+ }
+ term.set_cursor_position(
+ x: 0
+ y: 0
+ )
+ exit(0)
+}
+
+fn (mut a App) event(e &ui.Event) {
+ match e.typ {
+ .mouse_move {
+ if a.mode != .game {
+ return
+ }
+ // TODO mouse movement for real Pong sharks
+ // a.game.move_player(player_one, 0, -1)
+ }
+ .key_down {
+ match e.code {
+ .escape, .q {
+ a.quit()
+ }
+ .w {
+ if a.mode != .game {
+ return
+ }
+ a.game.move_player(player_one, 0, -1)
+ }
+ .a {
+ if a.mode != .game {
+ return
+ }
+ a.game.move_player(player_one, 0, -1)
+ }
+ .s {
+ if a.mode != .game {
+ return
+ }
+ a.game.move_player(player_one, 0, 1)
+ }
+ .d {
+ if a.mode != .game {
+ return
+ }
+ a.game.move_player(player_one, 0, 1)
+ }
+ .left {
+ if a.mode != .game {
+ return
+ }
+ a.game.move_player(player_two, 0, -1)
+ }
+ .right {
+ if a.mode != .game {
+ return
+ }
+ a.game.move_player(player_two, 0, 1)
+ }
+ .up {
+ if a.mode != .game {
+ return
+ }
+ a.game.move_player(player_two, 0, -1)
+ }
+ .down {
+ if a.mode != .game {
+ return
+ }
+ a.game.move_player(player_two, 0, 1)
+ }
+ .enter, .space {
+ if a.mode == .menu {
+ a.start_game()
+ }
+ }
+ else {}
+ }
+ }
+ else {}
+ }
+}
+
+fn (mut a App) free() {
+ unsafe {
+ a.game.free()
+ free(a.game)
+ }
+}
+
+fn (mut a App) render() {
+ match a.mode {
+ .menu { a.draw_menu() }
+ else { a.draw_game() }
+ }
+}
+
+fn (mut a App) draw_menu() {
+ cx := int(f32(a.width) * 0.5)
+ y025 := int(f32(a.height) * 0.25)
+ y075 := int(f32(a.height) * 0.75)
+ cy := int(f32(a.height) * 0.5)
+ //
+ a.tui.set_color(white)
+ a.tui.bold()
+ a.tui.draw_text(cx - 2, y025, 'VONG')
+ a.tui.reset()
+ a.tui.draw_text(cx - 13, y025 + 1, '(A game of Pong written in V)')
+ //
+ a.tui.set_color(white)
+ a.tui.bold()
+ a.tui.draw_text(cx - 3, cy + 1, 'START')
+ a.tui.reset()
+ //
+ a.tui.draw_text(cx - 9, y075 + 1, 'Press SPACE to start')
+ a.tui.reset()
+ a.tui.draw_text(cx - 5, y075 + 3, 'ESC to Quit')
+ a.tui.reset()
+}
+
+fn (mut a App) draw_game() {
+ a.game.draw()
+}
+
+struct Player {
+mut:
+ game &Game
+ pos Vec
+ racket_size int = 4
+ score int
+ ai bool
+}
+
+fn (mut p Player) move(x f32, y f32) {
+ p.pos.x += x
+ p.pos.y += y
+}
+
+fn (mut p Player) update() {
+ if !p.ai {
+ return
+ }
+ if isnil(p.game) {
+ return
+ }
+ // dt := p.game.app.dt
+ ball := unsafe { &p.game.ball }
+ // Evil AI that eventually will take over the world
+ p.pos.y = ball.pos.y - int(f32(p.racket_size) * 0.5)
+}
+
+struct Vec {
+mut:
+ x f32
+ y f32
+}
+
+fn (mut v Vec) set(x f32, y f32) {
+ v.x = x
+ v.y = y
+}
+
+struct Ball {
+mut:
+ pos Vec
+ vel Vec
+ acc Vec
+}
+
+fn (mut b Ball) update(dt f32) {
+ b.pos.x += b.vel.x * b.acc.x * dt
+ b.pos.y += b.vel.y * b.acc.y * dt
+}
+
+[heap]
+struct Game {
+mut:
+ app &App = 0
+ players []Player
+ ball Ball
+}
+
+fn (mut g Game) move_player(id int, x int, y int) {
+ mut p := unsafe { &g.players[id] }
+ if p.ai { // disable AI when moved
+ p.ai = false
+ }
+ p.move(x, y)
+}
+
+fn (mut g Game) init() {
+ if g.players.len == 0 {
+ g.players = []Player{len: 2, init: Player{ // <- BUG omitting the init will result in smaller racket sizes???
+ game: g
+ }}
+ }
+ g.reset()
+}
+
+fn (mut g Game) reset() {
+ mut i := 0
+ for mut p in g.players {
+ p.score = 0
+ if i != player_one {
+ p.ai = true
+ }
+ i++
+ }
+ g.new_round()
+}
+
+fn (mut g Game) new_round() {
+ mut i := 0
+ for mut p in g.players {
+ p.pos.x = if i == 0 { 3 } else { g.app.width - 2 }
+ p.pos.y = f32(g.app.height) * 0.5 - f32(p.racket_size) * 0.5
+ i++
+ }
+ g.ball.pos.set(f32(g.app.width) * 0.5, f32(g.app.height) * 0.5)
+ g.ball.vel.set(-8, -15)
+ g.ball.acc.set(2.0, 1.0)
+}
+
+fn (mut g Game) update() {
+ dt := g.app.dt
+ mut b := unsafe { &g.ball }
+ for mut p in g.players {
+ p.update()
+ // Keep rackets within the game area
+ if p.pos.y <= 0 {
+ p.pos.y = 1
+ }
+ if p.pos.y + p.racket_size >= g.app.height {
+ p.pos.y = g.app.height - p.racket_size - 1
+ }
+ // Check ball collision
+ // Player left side
+ if p.pos.x < f32(g.app.width) * 0.5 {
+ // Racket collision
+ if b.pos.x <= p.pos.x + 1 {
+ if b.pos.y >= p.pos.y && b.pos.y <= p.pos.y + p.racket_size {
+ b.vel.x *= -1
+ }
+ }
+ // Behind racket
+ if b.pos.x < p.pos.x {
+ g.players[1].score++
+ g.new_round()
+ }
+ } else {
+ // Player right side
+ if b.pos.x >= p.pos.x - 1 {
+ if b.pos.y >= p.pos.y && b.pos.y <= p.pos.y + p.racket_size {
+ b.vel.x *= -1
+ }
+ }
+ if b.pos.x > p.pos.x {
+ g.players[0].score++
+ g.new_round()
+ }
+ }
+ }
+ if b.pos.x <= 1 || b.pos.x >= g.app.width {
+ b.vel.x *= -1
+ }
+ if b.pos.y <= 2 || b.pos.y >= g.app.height {
+ b.vel.y *= -1
+ }
+ b.update(dt)
+}
+
+fn (mut g Game) quit() {
+ if g.app.mode != .game {
+ return
+ }
+ g.app.mode = .menu
+}
+
+fn (mut g Game) draw_big_digit(px f32, py f32, digit int) {
+ // TODO use draw_line or draw_point to fix tearing with non-monospaced terminal fonts
+ mut gfx := g.app.tui
+ x, y := int(px), int(py)
+ match digit {
+ 0 {
+ gfx.draw_text(x, y + 0, '█████')
+ gfx.draw_text(x, y + 1, '█ █')
+ gfx.draw_text(x, y + 2, '█ █')
+ gfx.draw_text(x, y + 3, '█ █')
+ gfx.draw_text(x, y + 4, '█████')
+ }
+ 1 {
+ gfx.draw_text(x + 3, y + 0, '█')
+ gfx.draw_text(x + 3, y + 1, '█')
+ gfx.draw_text(x + 3, y + 2, '█')
+ gfx.draw_text(x + 3, y + 3, '█')
+ gfx.draw_text(x + 3, y + 4, '█')
+ }
+ 2 {
+ gfx.draw_text(x, y + 0, '█████')
+ gfx.draw_text(x, y + 1, ' █')
+ gfx.draw_text(x, y + 2, '█████')
+ gfx.draw_text(x, y + 3, '█')
+ gfx.draw_text(x, y + 4, '█████')
+ }
+ 3 {
+ gfx.draw_text(x, y + 0, '█████')
+ gfx.draw_text(x, y + 1, ' ██')
+ gfx.draw_text(x, y + 2, ' ████')
+ gfx.draw_text(x, y + 3, ' ██')
+ gfx.draw_text(x, y + 4, '█████')
+ }
+ 4 {
+ gfx.draw_text(x, y + 0, '█ █')
+ gfx.draw_text(x, y + 1, '█ █')
+ gfx.draw_text(x, y + 2, '█████')
+ gfx.draw_text(x, y + 3, ' █')
+ gfx.draw_text(x, y + 4, ' █')
+ }
+ 5 {
+ gfx.draw_text(x, y + 0, '█████')
+ gfx.draw_text(x, y + 1, '█')
+ gfx.draw_text(x, y + 2, '█████')
+ gfx.draw_text(x, y + 3, ' █')
+ gfx.draw_text(x, y + 4, '█████')
+ }
+ 6 {
+ gfx.draw_text(x, y + 0, '█████')
+ gfx.draw_text(x, y + 1, '█')
+ gfx.draw_text(x, y + 2, '█████')
+ gfx.draw_text(x, y + 3, '█ █')
+ gfx.draw_text(x, y + 4, '█████')
+ }
+ 7 {
+ gfx.draw_text(x, y + 0, '█████')
+ gfx.draw_text(x, y + 1, ' █')
+ gfx.draw_text(x, y + 2, ' █')
+ gfx.draw_text(x, y + 3, ' █')
+ gfx.draw_text(x, y + 4, ' █')
+ }
+ 8 {
+ gfx.draw_text(x, y + 0, '█████')
+ gfx.draw_text(x, y + 1, '█ █')
+ gfx.draw_text(x, y + 2, '█████')
+ gfx.draw_text(x, y + 3, '█ █')
+ gfx.draw_text(x, y + 4, '█████')
+ }
+ 9 {
+ gfx.draw_text(x, y + 0, '█████')
+ gfx.draw_text(x, y + 1, '█ █')
+ gfx.draw_text(x, y + 2, '█████')
+ gfx.draw_text(x, y + 3, ' █')
+ gfx.draw_text(x, y + 4, '█████')
+ }
+ else {}
+ }
+}
+
+fn (mut g Game) draw() {
+ mut gfx := g.app.tui
+ gfx.set_bg_color(white)
+ // Border
+ gfx.draw_empty_rect(1, 1, g.app.width, g.app.height)
+ // Center line
+ gfx.draw_dashed_line(int(f32(g.app.width) * 0.5), 0, int(f32(g.app.width) * 0.5),
+ int(g.app.height))
+ border := 1
+ mut y, mut x := 0, 0
+ for p in g.players {
+ x = int(p.pos.x)
+ y = int(p.pos.y)
+ gfx.reset_bg_color()
+ gfx.set_color(white)
+ if x < f32(g.app.width) * 0.5 {
+ g.draw_big_digit(f32(g.app.width) * 0.25, 3, p.score)
+ } else {
+ g.draw_big_digit(f32(g.app.width) * 0.75, 3, p.score)
+ }
+ gfx.reset_color()
+ gfx.set_bg_color(white)
+ // Racket
+ gfx.draw_line(x, y + border, x, y + p.racket_size)
+ }
+ // Ball
+ gfx.draw_point(int(g.ball.pos.x), int(g.ball.pos.y))
+ // gfx.draw_text(22,2,'$g.ball.pos')
+ gfx.reset_bg_color()
+}
+
+fn (mut g Game) free() {
+ g.players.clear()
+}
+
+// TODO Remove these wrapper functions when we can assign methods as callbacks
+fn init(x voidptr) {
+ mut app := &App(x)
+ app.init()
+}
+
+fn frame(x voidptr) {
+ mut app := &App(x)
+ app.frame()
+}
+
+fn cleanup(x voidptr) {
+ mut app := &App(x)
+ app.free()
+}
+
+fn fail(error string) {
+ eprintln(error)
+}
+
+fn event(e &ui.Event, x voidptr) {
+ mut app := &App(x)
+ app.event(e)
+}
+
+fn main() {
+ mut app := &App{}
+ app.tui = ui.init(
+ user_data: app
+ init_fn: init
+ frame_fn: frame
+ cleanup_fn: cleanup
+ event_fn: event
+ fail_fn: fail
+ capture_events: true
+ hide_cursor: true
+ frame_rate: 60
+ )
+ app.tui.run() ?
+}
diff --git a/v_windows/v/examples/term.ui/rectangles.v b/v_windows/v/examples/term.ui/rectangles.v
new file mode 100644
index 0000000..09e9d6a
--- /dev/null
+++ b/v_windows/v/examples/term.ui/rectangles.v
@@ -0,0 +1,97 @@
+import term.ui as tui
+import rand
+
+struct Rect {
+mut:
+ c tui.Color
+ x int
+ y int
+ x2 int
+ y2 int
+}
+
+struct App {
+mut:
+ tui &tui.Context = 0
+ rects []Rect
+ cur_rect Rect
+ is_drag bool
+ redraw bool
+}
+
+fn random_color() tui.Color {
+ return tui.Color{
+ r: byte(rand.intn(256))
+ g: byte(rand.intn(256))
+ b: byte(rand.intn(256))
+ }
+}
+
+fn event(e &tui.Event, x voidptr) {
+ mut app := &App(x)
+ match e.typ {
+ .mouse_down {
+ app.is_drag = true
+ app.cur_rect = Rect{
+ c: random_color()
+ x: e.x
+ y: e.y
+ x2: e.x
+ y2: e.y
+ }
+ }
+ .mouse_drag {
+ app.cur_rect.x2 = e.x
+ app.cur_rect.y2 = e.y
+ }
+ .mouse_up {
+ app.rects << app.cur_rect
+ app.is_drag = false
+ }
+ .key_down {
+ if e.code == .c {
+ app.rects.clear()
+ } else if e.code == .escape {
+ exit(0)
+ }
+ }
+ else {}
+ }
+ app.redraw = true
+}
+
+fn frame(x voidptr) {
+ mut app := &App(x)
+ if !app.redraw {
+ return
+ }
+
+ app.tui.clear()
+
+ for rect in app.rects {
+ app.tui.set_bg_color(rect.c)
+ app.tui.draw_rect(rect.x, rect.y, rect.x2, rect.y2)
+ }
+
+ if app.is_drag {
+ r := app.cur_rect
+ app.tui.set_bg_color(r.c)
+ app.tui.draw_empty_rect(r.x, r.y, r.x2, r.y2)
+ }
+
+ app.tui.reset_bg_color()
+ app.tui.flush()
+ app.redraw = false
+}
+
+fn main() {
+ mut app := &App{}
+ app.tui = tui.init(
+ user_data: app
+ event_fn: event
+ frame_fn: frame
+ hide_cursor: true
+ frame_rate: 60
+ )
+ app.tui.run() ?
+}
diff --git a/v_windows/v/examples/term.ui/screenshot_pong.png b/v_windows/v/examples/term.ui/screenshot_pong.png
new file mode 100644
index 0000000..9127d7b
--- /dev/null
+++ b/v_windows/v/examples/term.ui/screenshot_pong.png
Binary files differ
diff --git a/v_windows/v/examples/term.ui/term_drawing.v b/v_windows/v/examples/term.ui/term_drawing.v
new file mode 100644
index 0000000..df08cb7
--- /dev/null
+++ b/v_windows/v/examples/term.ui/term_drawing.v
@@ -0,0 +1,510 @@
+// Copyright (c) 2020 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 main
+
+import term.ui
+
+// The color palette, taken from Google's Material design
+const (
+ colors = [
+ [
+ ui.Color{239, 154, 154},
+ ui.Color{244, 143, 177},
+ ui.Color{206, 147, 216},
+ ui.Color{179, 157, 219},
+ ui.Color{159, 168, 218},
+ ui.Color{144, 202, 249},
+ ui.Color{129, 212, 250},
+ ui.Color{128, 222, 234},
+ ui.Color{128, 203, 196},
+ ui.Color{165, 214, 167},
+ ui.Color{197, 225, 165},
+ ui.Color{230, 238, 156},
+ ui.Color{255, 245, 157},
+ ui.Color{255, 224, 130},
+ ui.Color{255, 204, 128},
+ ui.Color{255, 171, 145},
+ ui.Color{188, 170, 164},
+ ui.Color{238, 238, 238},
+ ui.Color{176, 190, 197},
+ ],
+ [
+ ui.Color{244, 67, 54},
+ ui.Color{233, 30, 99},
+ ui.Color{156, 39, 176},
+ ui.Color{103, 58, 183},
+ ui.Color{63, 81, 181},
+ ui.Color{33, 150, 243},
+ ui.Color{3, 169, 244},
+ ui.Color{0, 188, 212},
+ ui.Color{0, 150, 136},
+ ui.Color{76, 175, 80},
+ ui.Color{139, 195, 74},
+ ui.Color{205, 220, 57},
+ ui.Color{255, 235, 59},
+ ui.Color{255, 193, 7},
+ ui.Color{255, 152, 0},
+ ui.Color{255, 87, 34},
+ ui.Color{121, 85, 72},
+ ui.Color{120, 120, 120},
+ ui.Color{96, 125, 139},
+ ],
+ [
+ ui.Color{198, 40, 40},
+ ui.Color{173, 20, 87},
+ ui.Color{106, 27, 154},
+ ui.Color{69, 39, 160},
+ ui.Color{40, 53, 147},
+ ui.Color{21, 101, 192},
+ ui.Color{2, 119, 189},
+ ui.Color{0, 131, 143},
+ ui.Color{0, 105, 92},
+ ui.Color{46, 125, 50},
+ ui.Color{85, 139, 47},
+ ui.Color{158, 157, 36},
+ ui.Color{249, 168, 37},
+ ui.Color{255, 143, 0},
+ ui.Color{239, 108, 0},
+ ui.Color{216, 67, 21},
+ ui.Color{78, 52, 46},
+ ui.Color{33, 33, 33},
+ ui.Color{55, 71, 79},
+ ],
+ ]
+)
+
+const (
+ frame_rate = 30 // fps
+ msg_display_time = 5 * frame_rate
+ w = 200
+ h = 100
+ space = ' '
+ spaces = ' '
+ select_color = 'Select color: '
+ select_size = 'Size: + -'
+ help_1 = '╭────────╮'
+ help_2 = '│ HELP │'
+ help_3 = '╰────────╯'
+)
+
+struct App {
+mut:
+ ui &ui.Context = 0
+ header_text []string
+ mouse_pos Point
+ msg string
+ msg_hide_tick int
+ primary_color ui.Color = colors[1][6]
+ secondary_color ui.Color = colors[1][9]
+ primary_color_idx int = 25
+ secondary_color_idx int = 28
+ bg_color ui.Color = ui.Color{0, 0, 0}
+ drawing [][]ui.Color = [][]ui.Color{len: h, init: []ui.Color{len: w}}
+ size int = 1
+ should_redraw bool = true
+ is_dragging bool
+}
+
+struct Point {
+mut:
+ x int
+ y int
+}
+
+fn main() {
+ mut app := &App{}
+ app.ui = ui.init(
+ user_data: app
+ frame_fn: frame
+ event_fn: event
+ frame_rate: frame_rate
+ hide_cursor: true
+ window_title: 'V terminal pixelart drawing app'
+ )
+ app.mouse_pos.x = 40
+ app.mouse_pos.y = 15
+ app.ui.clear()
+ app.ui.run() ?
+}
+
+fn frame(x voidptr) {
+ mut app := &App(x)
+ mut redraw := app.should_redraw
+ if app.msg != '' && app.ui.frame_count >= app.msg_hide_tick {
+ app.msg = ''
+ redraw = true
+ }
+ if redraw {
+ app.render(false)
+ app.should_redraw = false
+ }
+}
+
+fn event(event &ui.Event, x voidptr) {
+ mut app := &App(x)
+ match event.typ {
+ .mouse_down {
+ app.is_dragging = true
+ if app.ui.window_height - event.y < 5 {
+ app.footer_click(event)
+ } else {
+ app.paint(event)
+ }
+ }
+ .mouse_up {
+ app.is_dragging = false
+ }
+ .mouse_drag {
+ app.mouse_pos = Point{
+ x: event.x
+ y: event.y
+ }
+ app.paint(event)
+ }
+ .mouse_move {
+ app.mouse_pos = Point{
+ x: event.x
+ y: event.y
+ }
+ }
+ .mouse_scroll {
+ app.mouse_pos = Point{
+ x: event.x
+ y: event.y
+ }
+ d := event.direction == .down
+ if event.modifiers.has(.ctrl) {
+ p := !event.modifiers.has(.shift)
+ c := if d {
+ if p { app.primary_color_idx - 1 } else { app.secondary_color_idx - 1 }
+ } else {
+ if p { app.primary_color_idx + 1 } else { app.secondary_color_idx + 1 }
+ }
+ app.select_color(p, c)
+ } else {
+ if d {
+ app.inc_size()
+ } else {
+ app.dec_size()
+ }
+ }
+ }
+ .key_down {
+ match event.code {
+ .f1, ._1 {
+ oevent := *event
+ nevent := ui.Event{
+ ...oevent
+ button: ui.MouseButton.left
+ x: app.mouse_pos.x
+ y: app.mouse_pos.y
+ }
+ app.paint(nevent)
+ }
+ .f2, ._2 {
+ oevent := *event
+ nevent := ui.Event{
+ ...oevent
+ button: ui.MouseButton.right
+ x: app.mouse_pos.x
+ y: app.mouse_pos.y
+ }
+ app.paint(nevent)
+ }
+ .space, .enter {
+ oevent := *event
+ nevent := ui.Event{
+ ...oevent
+ button: .left
+ x: app.mouse_pos.x
+ y: app.mouse_pos.y
+ }
+ app.paint(nevent)
+ }
+ .delete, .backspace {
+ oevent := *event
+ nevent := ui.Event{
+ ...oevent
+ button: .middle
+ x: app.mouse_pos.x
+ y: app.mouse_pos.y
+ }
+ app.paint(nevent)
+ }
+ .j, .down {
+ if event.modifiers.has(.shift) {
+ app.set_pixel((1 + app.mouse_pos.x) / 2, app.mouse_pos.y, app.primary_color)
+ }
+ app.mouse_pos.y++
+ }
+ .k, .up {
+ if event.modifiers.has(.shift) {
+ app.set_pixel((1 + app.mouse_pos.x) / 2, app.mouse_pos.y, app.primary_color)
+ }
+ app.mouse_pos.y--
+ }
+ .h, .left {
+ if event.modifiers.has(.shift) {
+ app.set_pixel((1 + app.mouse_pos.x) / 2, app.mouse_pos.y, app.primary_color)
+ }
+ app.mouse_pos.x -= 2
+ }
+ .l, .right {
+ if event.modifiers.has(.shift) {
+ app.set_pixel((1 + app.mouse_pos.x) / 2, app.mouse_pos.y, app.primary_color)
+ }
+ app.mouse_pos.x += 2
+ }
+ .t {
+ p := !event.modifiers.has(.alt)
+ c := if event.modifiers.has(.shift) {
+ if p { app.primary_color_idx - 19 } else { app.secondary_color_idx - 19 }
+ } else {
+ if p { app.primary_color_idx + 19 } else { app.secondary_color_idx + 19 }
+ }
+ app.select_color(p, c)
+ }
+ .r {
+ p := !event.modifiers.has(.alt)
+ c := if event.modifiers.has(.shift) {
+ if p { app.primary_color_idx - 1 } else { app.secondary_color_idx - 1 }
+ } else {
+ if p { app.primary_color_idx + 1 } else { app.secondary_color_idx + 1 }
+ }
+ app.select_color(p, c)
+ }
+ .plus {
+ app.inc_size()
+ }
+ .minus {
+ app.dec_size()
+ }
+ .c {
+ app.drawing = [][]ui.Color{len: h, init: []ui.Color{len: w}}
+ }
+ .q, .escape {
+ app.render(true)
+ exit(0)
+ }
+ else {}
+ }
+ }
+ else {}
+ }
+ app.should_redraw = true
+}
+
+fn (mut app App) render(paint_only bool) {
+ app.ui.clear()
+ app.draw_header()
+ app.draw_content()
+ if !paint_only {
+ app.draw_footer()
+ app.draw_cursor()
+ }
+ app.ui.flush()
+}
+
+fn (mut app App) select_color(primary bool, idx int) {
+ c := (idx + 57) % 57
+ cx := c % 19
+ cy := (c / 19) % 3
+ color := colors[cy][cx]
+ if primary {
+ app.primary_color_idx = c % (19 * 3)
+ app.primary_color = color
+ } else {
+ app.secondary_color_idx = c % (19 * 3)
+ app.secondary_color = color
+ }
+ c_str := if primary { 'primary' } else { 'secondary' }
+ app.show_msg('set $c_str color idx: $idx', 1)
+}
+
+fn (mut app App) set_pixel(x_ int, y_ int, c ui.Color) {
+ // Term coords start at 1, and adjust for the header
+ x, y := x_ - 1, y_ - 4
+ if y < 0 || app.ui.window_height - y < 3 {
+ return
+ }
+ if y >= app.drawing.len || x < 0 || x >= app.drawing[0].len {
+ return
+ }
+ app.drawing[y][x] = c
+}
+
+fn (mut app App) paint(event &ui.Event) {
+ if event.y < 4 || app.ui.window_height - event.y < 4 {
+ return
+ }
+ x_start, y_start := int(f32((event.x - 1) / 2) - app.size / 2 + 1), event.y - app.size / 2
+ color := match event.button {
+ .left { app.primary_color }
+ .right { app.secondary_color }
+ else { app.bg_color }
+ }
+ for x in x_start .. x_start + app.size {
+ for y in y_start .. y_start + app.size {
+ app.set_pixel(x, y, color)
+ }
+ }
+}
+
+fn (mut app App) draw_content() {
+ w_, mut h_ := app.ui.window_width / 2, app.ui.window_height - 8
+ if h_ > app.drawing.len {
+ h_ = app.drawing.len
+ }
+ for row_idx, row in app.drawing[..h_] {
+ app.ui.set_cursor_position(0, row_idx + 4)
+ mut last := ui.Color{0, 0, 0}
+ for cell in row[..w_] {
+ if cell.r == 0 && cell.g == 0 && cell.b == 0 {
+ if !(cell.r == last.r && cell.g == last.g && cell.b == last.b) {
+ app.ui.reset()
+ }
+ } else {
+ if !(cell.r == last.r && cell.g == last.g && cell.b == last.b) {
+ app.ui.set_bg_color(cell)
+ }
+ }
+ app.ui.write(spaces)
+ last = cell
+ }
+ app.ui.reset()
+ }
+}
+
+fn (mut app App) draw_cursor() {
+ if app.mouse_pos.y in [3, app.ui.window_height - 5] {
+ // inside the horizontal separators
+ return
+ }
+ cursor_color := if app.is_dragging { ui.Color{220, 220, 220} } else { ui.Color{160, 160, 160} }
+ app.ui.set_bg_color(cursor_color)
+ if app.mouse_pos.y >= 3 && app.mouse_pos.y <= app.ui.window_height - 4 {
+ // inside the main content
+ mut x_start := int(f32((app.mouse_pos.x - 1) / 2) - app.size / 2 + 1) * 2 - 1
+ mut y_start := app.mouse_pos.y - app.size / 2
+ mut x_end := x_start + app.size * 2 - 1
+ mut y_end := y_start + app.size - 1
+ if x_start < 1 {
+ x_start = 1
+ }
+ if y_start < 4 {
+ y_start = 4
+ }
+ if x_end > app.ui.window_width {
+ x_end = app.ui.window_width
+ }
+ if y_end > app.ui.window_height - 5 {
+ y_end = app.ui.window_height - 5
+ }
+ app.ui.draw_rect(x_start, y_start, x_end, y_end)
+ } else {
+ app.ui.draw_text(app.mouse_pos.x, app.mouse_pos.y, space)
+ }
+ app.ui.reset()
+}
+
+fn (mut app App) draw_header() {
+ if app.msg != '' {
+ app.ui.set_color(
+ r: 0
+ g: 0
+ b: 0
+ )
+ app.ui.set_bg_color(
+ r: 220
+ g: 220
+ b: 220
+ )
+ app.ui.draw_text(0, 0, ' $app.msg ')
+ app.ui.reset()
+ }
+ //'tick: $app.ui.frame_count | ' +
+ app.ui.draw_text(3, 2, 'terminal size: ($app.ui.window_width, $app.ui.window_height) | primary color: $app.primary_color.hex() | secondary color: $app.secondary_color.hex()')
+ app.ui.horizontal_separator(3)
+}
+
+fn (mut app App) draw_footer() {
+ _, wh := app.ui.window_width, app.ui.window_height
+ app.ui.horizontal_separator(wh - 4)
+ for i, color_row in colors {
+ for j, color in color_row {
+ x := j * 3 + 19
+ y := wh - 3 + i
+ app.ui.set_bg_color(color)
+ if app.primary_color_idx == j + (i * 19) {
+ app.ui.set_color(r: 0, g: 0, b: 0)
+ app.ui.draw_text(x, y, '><')
+ app.ui.reset_color()
+ } else if app.secondary_color_idx == j + (i * 19) {
+ app.ui.set_color(r: 255, g: 255, b: 255)
+ app.ui.draw_text(x, y, '><')
+ app.ui.reset_color()
+ } else {
+ app.ui.draw_rect(x, y, x + 1, y)
+ }
+ }
+ }
+ app.ui.reset_bg_color()
+ app.ui.draw_text(3, wh - 3, select_color)
+ app.ui.bold()
+ app.ui.draw_text(3, wh - 1, '$select_size $app.size')
+ app.ui.reset()
+
+ // TODO: help button
+ // if ww >= 90 {
+ // app.ui.draw_text(80, wh - 3, help_1)
+ // app.ui.draw_text(80, wh - 2, help_2)
+ // app.ui.draw_text(80, wh - 1, help_3)
+ // }
+}
+
+[inline]
+fn (mut app App) inc_size() {
+ if app.size < 30 {
+ app.size++
+ }
+ app.show_msg('inc. size: $app.size', 1)
+}
+
+[inline]
+fn (mut app App) dec_size() {
+ if app.size > 1 {
+ app.size--
+ }
+ app.show_msg('dec. size: $app.size', 1)
+}
+
+fn (mut app App) footer_click(event &ui.Event) {
+ footer_y := 3 - (app.ui.window_height - event.y)
+ match event.x {
+ 8...11 {
+ app.inc_size()
+ }
+ 12...15 {
+ app.dec_size()
+ }
+ 18...75 {
+ if (event.x % 3) == 0 {
+ // Inside the gap between tiles
+ return
+ }
+ idx := footer_y * 19 - 6 + event.x / 3
+ if idx < 0 || idx > 56 {
+ return
+ }
+ app.select_color(event.button == .left, idx)
+ }
+ else {}
+ }
+}
+
+fn (mut app App) show_msg(text string, time int) {
+ frames := time * frame_rate
+ app.msg_hide_tick = if time > 0 { int(app.ui.frame_count) + frames } else { -1 }
+ app.msg = text
+}
diff --git a/v_windows/v/examples/term.ui/text_editor.v b/v_windows/v/examples/term.ui/text_editor.v
new file mode 100644
index 0000000..78e6e40
--- /dev/null
+++ b/v_windows/v/examples/term.ui/text_editor.v
@@ -0,0 +1,583 @@
+// Copyright (c) 2020 Lars Pontoppidan. All rights reserved.
+// Use of this source code is governed by the MIT license distributed with this software.
+// Don't use this editor for any serious work.
+// A lot of funtionality is missing compared to your favourite editor :)
+import strings
+import math.mathutil as mu
+import os
+import term.ui as tui
+
+enum Movement {
+ up
+ down
+ left
+ right
+ home
+ end
+ page_up
+ page_down
+}
+
+struct View {
+pub:
+ raw string
+ cursor Cursor
+}
+
+struct App {
+mut:
+ tui &tui.Context = 0
+ ed &Buffer = 0
+ current_file int
+ files []string
+ status string
+ t int
+ magnet_x int
+ footer_height int = 2
+ viewport int
+}
+
+fn (mut a App) set_status(msg string, duration_ms int) {
+ a.status = msg
+ a.t = duration_ms
+}
+
+fn (mut a App) save() {
+ if a.cfile().len > 0 {
+ b := a.ed
+ os.write_file(a.cfile(), b.raw()) or { panic(err) }
+ a.set_status('Saved', 2000)
+ } else {
+ a.set_status('No file loaded', 4000)
+ }
+}
+
+fn (mut a App) cfile() string {
+ if a.files.len == 0 {
+ return ''
+ }
+ if a.current_file >= a.files.len {
+ return ''
+ }
+ return a.files[a.current_file]
+}
+
+fn (mut a App) visit_prev_file() {
+ if a.files.len == 0 {
+ a.current_file = 0
+ } else {
+ a.current_file = (a.current_file + a.files.len - 1) % a.files.len
+ }
+ a.init_file()
+}
+
+fn (mut a App) visit_next_file() {
+ if a.files.len == 0 {
+ a.current_file = 0
+ } else {
+ a.current_file = (a.current_file + a.files.len + 1) % a.files.len
+ }
+ a.init_file()
+}
+
+fn (mut a App) footer() {
+ w, h := a.tui.window_width, a.tui.window_height
+ mut b := a.ed
+ // flat := b.flat()
+ // snip := if flat.len > 19 { flat[..20] } else { flat }
+ finfo := if a.cfile().len > 0 { ' (' + os.file_name(a.cfile()) + ')' } else { '' }
+ mut status := a.status
+ a.tui.draw_text(0, h - 1, '─'.repeat(w))
+ footer := '$finfo Line ${b.cursor.pos_y + 1:4}/${b.lines.len:-4}, Column ${b.cursor.pos_x + 1:3}/${b.cur_line().len:-3} index: ${b.cursor_index():5} (ESC = quit, Ctrl+s = save)'
+ if footer.len < w {
+ a.tui.draw_text((w - footer.len) / 2, h, footer)
+ } else if footer.len == w {
+ a.tui.draw_text(0, h, footer)
+ } else {
+ a.tui.draw_text(0, h, footer[..w])
+ }
+ if a.t <= 0 {
+ status = ''
+ } else {
+ a.tui.set_bg_color(
+ r: 200
+ g: 200
+ b: 200
+ )
+ a.tui.set_color(
+ r: 0
+ g: 0
+ b: 0
+ )
+ a.tui.draw_text((w + 4 - status.len) / 2, h - 1, ' $status ')
+ a.tui.reset()
+ a.t -= 33
+ }
+}
+
+struct Buffer {
+ tab_width int = 4
+pub mut:
+ lines []string
+ cursor Cursor
+}
+
+fn (b Buffer) flat() string {
+ return b.raw().replace_each(['\n', r'\n', '\t', r'\t'])
+}
+
+fn (b Buffer) raw() string {
+ return b.lines.join('\n')
+}
+
+fn (b Buffer) view(from int, to int) View {
+ l := b.cur_line()
+ mut x := 0
+ for i := 0; i < b.cursor.pos_x && i < l.len; i++ {
+ if l[i] == `\t` {
+ x += b.tab_width
+ continue
+ }
+ x++
+ }
+ mut lines := []string{}
+ for i, line in b.lines {
+ if i >= from && i <= to {
+ lines << line
+ }
+ }
+ raw := lines.join('\n')
+ return View{
+ raw: raw.replace('\t', strings.repeat(` `, b.tab_width))
+ cursor: Cursor{
+ pos_x: x
+ pos_y: b.cursor.pos_y
+ }
+ }
+}
+
+fn (b Buffer) line(i int) string {
+ if i < 0 || i >= b.lines.len {
+ return ''
+ }
+ return b.lines[i]
+}
+
+fn (b Buffer) cur_line() string {
+ return b.line(b.cursor.pos_y)
+}
+
+fn (b Buffer) cursor_index() int {
+ mut i := 0
+ for y, line in b.lines {
+ if b.cursor.pos_y == y {
+ i += b.cursor.pos_x
+ break
+ }
+ i += line.len + 1
+ }
+ return i
+}
+
+fn (mut b Buffer) put(s string) {
+ has_line_ending := s.contains('\n')
+ x, y := b.cursor.xy()
+ if b.lines.len == 0 {
+ b.lines.prepend('')
+ }
+ line := b.lines[y]
+ l, r := line[..x], line[x..]
+ if has_line_ending {
+ mut lines := s.split('\n')
+ lines[0] = l + lines[0]
+ lines[lines.len - 1] += r
+ b.lines.delete(y)
+ b.lines.insert(y, lines)
+ last := lines[lines.len - 1]
+ b.cursor.set(last.len, y + lines.len - 1)
+ if s == '\n' {
+ b.cursor.set(0, b.cursor.pos_y)
+ }
+ } else {
+ b.lines[y] = l + s + r
+ b.cursor.set(x + s.len, y)
+ }
+ $if debug {
+ flat := s.replace('\n', r'\n')
+ eprintln(@MOD + '.' + @STRUCT + '::' + @FN + ' "$flat"')
+ }
+}
+
+fn (mut b Buffer) del(amount int) string {
+ if amount == 0 {
+ return ''
+ }
+ x, y := b.cursor.xy()
+ if amount < 0 { // don't delete left if we're at 0,0
+ if x == 0 && y == 0 {
+ return ''
+ }
+ } else if x >= b.cur_line().len && y >= b.lines.len - 1 {
+ return ''
+ }
+ mut removed := ''
+ if amount < 0 { // backspace (backward)
+ i := b.cursor_index()
+ removed = b.raw()[i + amount..i]
+ mut left := amount * -1
+ for li := y; li >= 0 && left > 0; li-- {
+ ln := b.lines[li]
+ if left > ln.len {
+ b.lines.delete(li)
+ if ln.len == 0 { // line break delimiter
+ left--
+ if y == 0 {
+ return ''
+ }
+ line_above := b.lines[li - 1]
+ b.cursor.pos_x = line_above.len
+ } else {
+ left -= ln.len
+ }
+ b.cursor.pos_y--
+ } else {
+ if x == 0 {
+ if y == 0 {
+ return ''
+ }
+ line_above := b.lines[li - 1]
+ if ln.len == 0 { // at line break
+ b.lines.delete(li)
+ b.cursor.pos_y--
+ b.cursor.pos_x = line_above.len
+ } else {
+ b.lines[li - 1] = line_above + ln
+ b.lines.delete(li)
+ b.cursor.pos_y--
+ b.cursor.pos_x = line_above.len
+ }
+ } else if x == 1 {
+ b.lines[li] = b.lines[li][left..]
+ b.cursor.pos_x = 0
+ } else {
+ b.lines[li] = ln[..x - left] + ln[x..]
+ b.cursor.pos_x -= left
+ }
+ left = 0
+ break
+ }
+ }
+ } else { // delete (forward)
+ i := b.cursor_index() + 1
+ removed = b.raw()[i - amount..i]
+ mut left := amount
+ for li := y; li >= 0 && left > 0; li++ {
+ ln := b.lines[li]
+ if x == ln.len { // at line end
+ if y + 1 <= b.lines.len {
+ b.lines[li] = ln + b.lines[y + 1]
+ b.lines.delete(y + 1)
+ left--
+ b.del(left)
+ }
+ } else if left > ln.len {
+ b.lines.delete(li)
+ left -= ln.len
+ } else {
+ b.lines[li] = ln[..x] + ln[x + left..]
+ left = 0
+ }
+ }
+ }
+ $if debug {
+ flat := removed.replace('\n', r'\n')
+ eprintln(@MOD + '.' + @STRUCT + '::' + @FN + ' "$flat"')
+ }
+ return removed
+}
+
+fn (mut b Buffer) free() {
+ $if debug {
+ eprintln(@MOD + '.' + @STRUCT + '::' + @FN)
+ }
+ for line in b.lines {
+ unsafe { line.free() }
+ }
+ unsafe { b.lines.free() }
+}
+
+fn (mut b Buffer) move_updown(amount int) {
+ b.cursor.move(0, amount)
+ // Check the move
+ line := b.cur_line()
+ if b.cursor.pos_x > line.len {
+ b.cursor.set(line.len, b.cursor.pos_y)
+ }
+}
+
+// move_cursor will navigate the cursor within the buffer bounds
+fn (mut b Buffer) move_cursor(amount int, movement Movement) {
+ cur_line := b.cur_line()
+ match movement {
+ .up {
+ if b.cursor.pos_y - amount >= 0 {
+ b.move_updown(-amount)
+ }
+ }
+ .down {
+ if b.cursor.pos_y + amount < b.lines.len {
+ b.move_updown(amount)
+ }
+ }
+ .page_up {
+ dlines := mu.min(b.cursor.pos_y, amount)
+ b.move_updown(-dlines)
+ }
+ .page_down {
+ dlines := mu.min(b.lines.len - 1, b.cursor.pos_y + amount) - b.cursor.pos_y
+ b.move_updown(dlines)
+ }
+ .left {
+ if b.cursor.pos_x - amount >= 0 {
+ b.cursor.move(-amount, 0)
+ } else if b.cursor.pos_y > 0 {
+ b.cursor.set(b.line(b.cursor.pos_y - 1).len, b.cursor.pos_y - 1)
+ }
+ }
+ .right {
+ if b.cursor.pos_x + amount <= cur_line.len {
+ b.cursor.move(amount, 0)
+ } else if b.cursor.pos_y + 1 < b.lines.len {
+ b.cursor.set(0, b.cursor.pos_y + 1)
+ }
+ }
+ .home {
+ b.cursor.set(0, b.cursor.pos_y)
+ }
+ .end {
+ b.cursor.set(cur_line.len, b.cursor.pos_y)
+ }
+ }
+}
+
+fn (mut b Buffer) move_to_word(movement Movement) {
+ a := if movement == .left { -1 } else { 1 }
+ mut line := b.cur_line()
+ mut x, mut y := b.cursor.pos_x, b.cursor.pos_y
+ if x + a < 0 && y > 0 {
+ y--
+ line = b.line(b.cursor.pos_y - 1)
+ x = line.len
+ } else if x + a >= line.len && y + 1 < b.lines.len {
+ y++
+ line = b.line(b.cursor.pos_y + 1)
+ x = 0
+ }
+ // first, move past all non-`a-zA-Z0-9_` characters
+ for x + a >= 0 && x + a < line.len && !(line[x + a].is_letter()
+ || line[x + a].is_digit() || line[x + a] == `_`) {
+ x += a
+ }
+ // then, move past all the letters and numbers
+ for x + a >= 0 && x + a < line.len && (line[x + a].is_letter()
+ || line[x + a].is_digit() || line[x + a] == `_`) {
+ x += a
+ }
+ // if the cursor is out of bounds, move it to the next/previous line
+ if x + a >= 0 && x + a <= line.len {
+ x += a
+ } else if a < 0 && y + 1 > b.lines.len && y - 1 >= 0 {
+ y += a
+ x = 0
+ }
+ b.cursor.set(x, y)
+}
+
+struct Cursor {
+pub mut:
+ pos_x int
+ pos_y int
+}
+
+fn (mut c Cursor) set(x int, y int) {
+ c.pos_x = x
+ c.pos_y = y
+}
+
+fn (mut c Cursor) move(x int, y int) {
+ c.pos_x += x
+ c.pos_y += y
+}
+
+fn (c Cursor) xy() (int, int) {
+ return c.pos_x, c.pos_y
+}
+
+// App callbacks
+fn init(x voidptr) {
+ mut a := &App(x)
+ a.init_file()
+}
+
+fn (mut a App) init_file() {
+ a.ed = &Buffer{}
+ mut init_y := 0
+ mut init_x := 0
+ if a.files.len > 0 && a.current_file < a.files.len && a.files[a.current_file].len > 0 {
+ if !os.is_file(a.files[a.current_file]) && a.files[a.current_file].contains(':') {
+ // support the file:line:col: format
+ fparts := a.files[a.current_file].split(':')
+ if fparts.len > 0 {
+ a.files[a.current_file] = fparts[0]
+ }
+ if fparts.len > 1 {
+ init_y = fparts[1].int() - 1
+ }
+ if fparts.len > 2 {
+ init_x = fparts[2].int() - 1
+ }
+ }
+ if os.is_file(a.files[a.current_file]) {
+ // 'vico: ' +
+ a.tui.set_window_title(a.files[a.current_file])
+ mut b := a.ed
+ content := os.read_file(a.files[a.current_file]) or { panic(err) }
+ b.put(content)
+ a.ed.cursor.pos_x = init_x
+ a.ed.cursor.pos_y = init_y
+ }
+ }
+}
+
+fn (a &App) view_height() int {
+ return a.tui.window_height - a.footer_height - 1
+}
+
+// magnet_cursor_x will place the cursor as close to it's last move left or right as possible
+fn (mut a App) magnet_cursor_x() {
+ mut buffer := a.ed
+ if buffer.cursor.pos_x < a.magnet_x {
+ if a.magnet_x < buffer.cur_line().len {
+ move_x := a.magnet_x - buffer.cursor.pos_x
+ buffer.move_cursor(move_x, .right)
+ }
+ }
+}
+
+fn frame(x voidptr) {
+ mut a := &App(x)
+ mut ed := a.ed
+ a.tui.clear()
+ scroll_limit := a.view_height()
+ // scroll down
+ if ed.cursor.pos_y > a.viewport + scroll_limit { // scroll down
+ a.viewport = ed.cursor.pos_y - scroll_limit
+ } else if ed.cursor.pos_y < a.viewport { // scroll up
+ a.viewport = ed.cursor.pos_y
+ }
+ view := ed.view(a.viewport, scroll_limit + a.viewport)
+ a.tui.draw_text(0, 0, view.raw)
+ a.footer()
+ a.tui.set_cursor_position(view.cursor.pos_x + 1, ed.cursor.pos_y + 1 - a.viewport)
+ a.tui.flush()
+}
+
+fn event(e &tui.Event, x voidptr) {
+ mut a := &App(x)
+ mut buffer := a.ed
+ if e.typ == .key_down {
+ match e.code {
+ .escape {
+ exit(0)
+ }
+ .enter {
+ buffer.put('\n')
+ }
+ .backspace {
+ buffer.del(-1)
+ }
+ .delete {
+ buffer.del(1)
+ }
+ .left {
+ if e.modifiers == .ctrl {
+ buffer.move_to_word(.left)
+ } else if e.modifiers.is_empty() {
+ buffer.move_cursor(1, .left)
+ }
+ a.magnet_x = buffer.cursor.pos_x
+ }
+ .right {
+ if e.modifiers == .ctrl {
+ buffer.move_to_word(.right)
+ } else if e.modifiers.is_empty() {
+ buffer.move_cursor(1, .right)
+ }
+ a.magnet_x = buffer.cursor.pos_x
+ }
+ .up {
+ buffer.move_cursor(1, .up)
+ a.magnet_cursor_x()
+ }
+ .down {
+ buffer.move_cursor(1, .down)
+ a.magnet_cursor_x()
+ }
+ .page_up {
+ buffer.move_cursor(a.view_height(), .page_up)
+ }
+ .page_down {
+ buffer.move_cursor(a.view_height(), .page_down)
+ }
+ .home {
+ buffer.move_cursor(1, .home)
+ }
+ .end {
+ buffer.move_cursor(1, .end)
+ }
+ 48...57, 97...122 { // 0-9a-zA-Z
+ if e.modifiers == .ctrl {
+ if e.code == .s {
+ a.save()
+ }
+ } else if !(e.modifiers.has(.ctrl | .alt) || e.code == .null) {
+ buffer.put(e.ascii.ascii_str())
+ }
+ }
+ else {
+ if e.modifiers == .alt {
+ if e.code == .comma {
+ a.visit_prev_file()
+ return
+ }
+ if e.code == .period {
+ a.visit_next_file()
+ return
+ }
+ }
+ buffer.put(e.utf8.bytes().bytestr())
+ }
+ }
+ } else if e.typ == .mouse_scroll {
+ direction := if e.direction == .up { Movement.down } else { Movement.up }
+ buffer.move_cursor(1, direction)
+ }
+}
+
+fn main() {
+ mut files := []string{}
+ if os.args.len > 1 {
+ files << os.args[1..]
+ }
+ mut a := &App{
+ files: files
+ }
+ a.tui = tui.init(
+ user_data: a
+ init_fn: init
+ frame_fn: frame
+ event_fn: event
+ capture_events: true
+ )
+ a.tui.run() ?
+}
diff --git a/v_windows/v/examples/term.ui/vyper.v b/v_windows/v/examples/term.ui/vyper.v
new file mode 100644
index 0000000..cf41e60
--- /dev/null
+++ b/v_windows/v/examples/term.ui/vyper.v
@@ -0,0 +1,475 @@
+// import modules for use in app
+import term.ui as termui
+import rand
+
+// define some global constants
+const (
+ block_size = 1
+ buffer = 10
+ green = termui.Color{0, 255, 0}
+ grey = termui.Color{150, 150, 150}
+ white = termui.Color{255, 255, 255}
+ blue = termui.Color{0, 0, 255}
+ red = termui.Color{255, 0, 0}
+ black = termui.Color{0, 0, 0}
+)
+
+// what edge of the screen are you facing
+enum Orientation {
+ top
+ right
+ bottom
+ left
+}
+
+// what's the current state of the game
+enum GameState {
+ pause
+ gameover
+ game
+ oob // snake out-of-bounds
+}
+
+// simple 2d vector representation
+struct Vec {
+mut:
+ x int
+ y int
+}
+
+// determine orientation from vector (hacky way to set facing from velocity)
+fn (v Vec) facing() Orientation {
+ result := if v.x >= 0 {
+ Orientation.right
+ } else if v.x < 0 {
+ Orientation.left
+ } else if v.y >= 0 {
+ Orientation.bottom
+ } else {
+ Orientation.top
+ }
+ return result
+}
+
+// generate a random vector with x in [min_x, max_x] and y in [min_y, max_y]
+fn (mut v Vec) randomize(min_x int, min_y int, max_x int, max_y int) {
+ v.x = rand.int_in_range(min_x, max_x)
+ v.y = rand.int_in_range(min_y, max_y)
+}
+
+// part of snake's body representation
+struct BodyPart {
+mut:
+ pos Vec = Vec{
+ x: block_size
+ y: block_size
+ }
+ color termui.Color = green
+ facing Orientation = .top
+}
+
+// snake representation
+struct Snake {
+mut:
+ app &App
+ direction Orientation
+ body []BodyPart
+ velocity Vec = Vec{
+ x: 0
+ y: 0
+ }
+}
+
+// length returns the snake's current length
+fn (s Snake) length() int {
+ return s.body.len
+}
+
+// impulse provides a impulse to change the snake's direction
+fn (mut s Snake) impulse(direction Orientation) {
+ mut vec := Vec{}
+ match direction {
+ .top {
+ vec.x = 0
+ vec.y = -1 * block_size
+ }
+ .right {
+ vec.x = 2 * block_size
+ vec.y = 0
+ }
+ .bottom {
+ vec.x = 0
+ vec.y = block_size
+ }
+ .left {
+ vec.x = -2 * block_size
+ vec.y = 0
+ }
+ }
+ s.direction = direction
+ s.velocity = vec
+}
+
+// move performs the calculations for the snake's movements
+fn (mut s Snake) move() {
+ mut i := s.body.len - 1
+ width := s.app.width
+ height := s.app.height
+ // move the parts of the snake as appropriate
+ for i = s.body.len - 1; i >= 0; i-- {
+ mut piece := s.body[i]
+ if i > 0 { // just move the body of the snake up one position
+ piece.pos = s.body[i - 1].pos
+ piece.facing = s.body[i - 1].facing
+ } else { // verify that the move is valid and move the head if so
+ piece.facing = s.direction
+ new_x := piece.pos.x + s.velocity.x
+ new_y := piece.pos.y + s.velocity.y
+ piece.pos.x += if new_x > block_size && new_x < width - block_size {
+ s.velocity.x
+ } else {
+ 0
+ }
+ piece.pos.y += if new_y > block_size && new_y < height - block_size {
+ s.velocity.y
+ } else {
+ 0
+ }
+ }
+ s.body[i] = piece
+ }
+}
+
+// grow add another part to the snake when it catches the rat
+fn (mut s Snake) grow() {
+ head := s.get_tail()
+ mut pos := Vec{}
+ // add the segment on the opposite side of the previous tail
+ match head.facing {
+ .bottom {
+ pos.x = head.pos.x
+ pos.y = head.pos.y - block_size
+ }
+ .left {
+ pos.x = head.pos.x + block_size
+ pos.y = head.pos.y
+ }
+ .top {
+ pos.x = head.pos.x
+ pos.y = head.pos.y + block_size
+ }
+ .right {
+ pos.x = head.pos.x - block_size
+ pos.y = head.pos.y
+ }
+ }
+ s.body << BodyPart{
+ pos: pos
+ facing: head.facing
+ }
+}
+
+// get_body gets the parts of the snakes body
+fn (s Snake) get_body() []BodyPart {
+ return s.body
+}
+
+// get_head get snake's head
+fn (s Snake) get_head() BodyPart {
+ return s.body[0]
+}
+
+// get_tail get snake's tail
+fn (s Snake) get_tail() BodyPart {
+ return s.body[s.body.len - 1]
+}
+
+// randomize randomizes position and veolcity of snake
+fn (mut s Snake) randomize() {
+ speeds := [-2, 0, 2]
+ mut pos := s.get_head().pos
+ pos.randomize(buffer, buffer, s.app.width - buffer, s.app.height - buffer)
+ for pos.x % 2 != 0 || (pos.x < buffer && pos.x > s.app.width - buffer) {
+ pos.randomize(buffer, buffer, s.app.width - buffer, s.app.height - buffer)
+ }
+ s.velocity.y = rand.int_in_range(-1 * block_size, block_size)
+ s.velocity.x = speeds[rand.intn(speeds.len)]
+ s.direction = s.velocity.facing()
+ s.body[0].pos = pos
+}
+
+// check_overlap determine if the snake's looped onto itself
+fn (s Snake) check_overlap() bool {
+ h := s.get_head()
+ head_pos := h.pos
+ for i in 2 .. s.length() {
+ piece_pos := s.body[i].pos
+ if head_pos.x == piece_pos.x && head_pos.y == piece_pos.y {
+ return true
+ }
+ }
+ return false
+}
+
+fn (s Snake) check_out_of_bounds() bool {
+ h := s.get_head()
+ return h.pos.x + s.velocity.x <= block_size
+ || h.pos.x + s.velocity.x > s.app.width - s.velocity.x
+ || h.pos.y + s.velocity.y <= block_size
+ || h.pos.y + s.velocity.y > s.app.height - block_size - s.velocity.y
+}
+
+// draw draws the parts of the snake
+fn (s Snake) draw() {
+ mut a := s.app
+ for part in s.get_body() {
+ a.termui.set_bg_color(part.color)
+ a.termui.draw_rect(part.pos.x, part.pos.y, part.pos.x + block_size, part.pos.y + block_size)
+ $if verbose ? {
+ text := match part.facing {
+ .top { '^' }
+ .bottom { 'v' }
+ .right { '>' }
+ .left { '<' }
+ }
+ a.termui.set_color(white)
+ a.termui.draw_text(part.pos.x, part.pos.y, text)
+ }
+ }
+}
+
+// rat representation
+struct Rat {
+mut:
+ pos Vec = Vec{
+ x: block_size
+ y: block_size
+ }
+ captured bool
+ color termui.Color = grey
+ app &App
+}
+
+// randomize spawn the rat in a new spot within the playable field
+fn (mut r Rat) randomize() {
+ r.pos.randomize(2 * block_size + buffer, 2 * block_size + buffer, r.app.width - block_size - buffer,
+ r.app.height - block_size - buffer)
+}
+
+[heap]
+struct App {
+mut:
+ termui &termui.Context = 0
+ snake Snake
+ rat Rat
+ width int
+ height int
+ redraw bool = true
+ state GameState = .game
+}
+
+// new_game setups the rat and snake for play
+fn (mut a App) new_game() {
+ mut snake := Snake{
+ body: []BodyPart{len: 1, init: BodyPart{}}
+ app: a
+ }
+ snake.randomize()
+ mut rat := Rat{
+ app: a
+ }
+ rat.randomize()
+ a.snake = snake
+ a.rat = rat
+ a.state = .game
+ a.redraw = true
+}
+
+// initialize the app and record the width and height of the window
+fn init(x voidptr) {
+ mut app := &App(x)
+ w, h := app.termui.window_width, app.termui.window_height
+ app.width = w
+ app.height = h
+ app.new_game()
+}
+
+// event handles different events for the app as they occur
+fn event(e &termui.Event, x voidptr) {
+ mut app := &App(x)
+ match e.typ {
+ .mouse_down {}
+ .mouse_drag {}
+ .mouse_up {}
+ .key_down {
+ match e.code {
+ .up, .w { app.move_snake(.top) }
+ .down, .s { app.move_snake(.bottom) }
+ .left, .a { app.move_snake(.left) }
+ .right, .d { app.move_snake(.right) }
+ .r { app.new_game() }
+ .c {}
+ .p { app.state = if app.state == .game { GameState.pause } else { GameState.game } }
+ .escape, .q { exit(0) }
+ else { exit(0) }
+ }
+ if e.code == .c {
+ } else if e.code == .escape {
+ exit(0)
+ }
+ }
+ else {}
+ }
+ app.redraw = true
+}
+
+// frame perform actions on every tick
+fn frame(x voidptr) {
+ mut app := &App(x)
+ app.update()
+ app.draw()
+}
+
+// update perform any calculations that are needed before drawing
+fn (mut a App) update() {
+ if a.state == .game {
+ a.snake.move()
+ if a.snake.check_out_of_bounds() {
+ $if verbose ? {
+ a.snake.body[0].color = red
+ } $else {
+ a.state = .oob
+ }
+ }
+ if a.snake.check_overlap() {
+ a.state = .gameover
+ return
+ }
+ if a.check_capture() {
+ a.rat.randomize()
+ a.snake.grow()
+ }
+ }
+}
+
+// draw write to the screen
+fn (mut a App) draw() {
+ // reset screen
+ a.termui.clear()
+ a.termui.set_bg_color(white)
+ a.termui.draw_empty_rect(1, 1, a.width, a.height)
+ // determine if a special screen needs to be draw
+ match a.state {
+ .gameover {
+ a.draw_gameover()
+ a.redraw = false
+ }
+ .pause {
+ a.draw_pause()
+ }
+ else {
+ a.redraw = true
+ }
+ }
+ a.termui.set_color(blue)
+ a.termui.set_bg_color(white)
+ a.termui.draw_text(3 * block_size, a.height - (2 * block_size), 'p - (un)pause r - reset q - quit')
+ // draw the snake, rat, and score if appropriate
+ if a.redraw {
+ a.termui.set_bg_color(black)
+ a.draw_gamescreen()
+ if a.state == .oob {
+ a.state = .gameover
+ }
+ }
+ // write to the screen
+ a.termui.reset_bg_color()
+ a.termui.flush()
+}
+
+// move_snake move the snake in specified direction
+fn (mut a App) move_snake(direction Orientation) {
+ a.snake.impulse(direction)
+}
+
+// check_capture determine if the snake overlaps with the rat
+fn (a App) check_capture() bool {
+ snake_pos := a.snake.get_head().pos
+ rat_pos := a.rat.pos
+ return snake_pos.x <= rat_pos.x + block_size && snake_pos.x + block_size >= rat_pos.x
+ && snake_pos.y <= rat_pos.y + block_size && snake_pos.y + block_size >= rat_pos.y
+}
+
+fn (mut a App) draw_snake() {
+ a.snake.draw()
+}
+
+fn (mut a App) draw_rat() {
+ a.termui.set_bg_color(a.rat.color)
+ a.termui.draw_rect(a.rat.pos.x, a.rat.pos.y, a.rat.pos.x + block_size, a.rat.pos.y + block_size)
+}
+
+fn (mut a App) draw_gamescreen() {
+ $if verbose ? {
+ a.draw_debug()
+ }
+ a.draw_score()
+ a.draw_rat()
+ a.draw_snake()
+}
+
+fn (mut a App) draw_score() {
+ a.termui.set_color(blue)
+ a.termui.set_bg_color(white)
+ score := a.snake.length() - 1
+ a.termui.draw_text(a.width - (2 * block_size), block_size, '${score:03d}')
+}
+
+fn (mut a App) draw_pause() {
+ a.termui.set_color(blue)
+ a.termui.draw_text((a.width / 2) - block_size, 3 * block_size, 'Paused!')
+}
+
+fn (mut a App) draw_debug() {
+ a.termui.set_color(blue)
+ a.termui.set_bg_color(white)
+ snake := a.snake
+ a.termui.draw_text(block_size, 1 * block_size, 'Display_width: ${a.width:04d} Display_height: ${a.height:04d}')
+ a.termui.draw_text(block_size, 2 * block_size, 'Vx: ${snake.velocity.x:+02d} Vy: ${snake.velocity.y:+02d}')
+ a.termui.draw_text(block_size, 3 * block_size, 'F: $snake.direction')
+ snake_head := snake.get_head()
+ rat := a.rat
+ a.termui.draw_text(block_size, 4 * block_size, 'Sx: ${snake_head.pos.x:+03d} Sy: ${snake_head.pos.y:+03d}')
+ a.termui.draw_text(block_size, 5 * block_size, 'Rx: ${rat.pos.x:+03d} Ry: ${rat.pos.y:+03d}')
+}
+
+fn (mut a App) draw_gameover() {
+ a.termui.set_bg_color(white)
+ a.termui.set_color(red)
+ a.rat.pos = Vec{
+ x: -1
+ y: -1
+ }
+ x_offset := ' ##### '.len // take half of a line from the game over text and store the length
+ start_x := (a.width / 2) - x_offset
+ a.termui.draw_text(start_x, (a.height / 2) - 3 * block_size, ' ##### ####### ')
+ a.termui.draw_text(start_x, (a.height / 2) - 2 * block_size, ' # # ## # # ###### # # # # ###### ##### ')
+ a.termui.draw_text(start_x, (a.height / 2) - 1 * block_size, ' # # # ## ## # # # # # # # # ')
+ a.termui.draw_text(start_x, (a.height / 2) - 0 * block_size, ' # #### # # # ## # ##### # # # # ##### # # ')
+ a.termui.draw_text(start_x, (a.height / 2) + 1 * block_size, ' # # ###### # # # # # # # # ##### ')
+ a.termui.draw_text(start_x, (a.height / 2) + 2 * block_size, ' # # # # # # # # # # # # # # ')
+ a.termui.draw_text(start_x, (a.height / 2) + 3 * block_size, ' ##### # # # # ###### ####### ## ###### # # ')
+}
+
+fn main() {
+ mut app := &App{}
+ app.termui = termui.init(
+ user_data: app
+ event_fn: event
+ frame_fn: frame
+ init_fn: init
+ hide_cursor: true
+ frame_rate: 10
+ )
+ app.termui.run() ?
+}