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/examples/term.ui | |
download | cli-tools-windows-f5c4671bfbad96bf346bd7e9a21fc4317b4959df.tar.gz cli-tools-windows-f5c4671bfbad96bf346bd7e9a21fc4317b4959df.tar.bz2 cli-tools-windows-f5c4671bfbad96bf346bd7e9a21fc4317b4959df.zip |
Diffstat (limited to 'v_windows/v/examples/term.ui')
-rw-r--r-- | v_windows/v/examples/term.ui/README.md | 1 | ||||
-rw-r--r-- | v_windows/v/examples/term.ui/cursor_chaser.v | 100 | ||||
-rw-r--r-- | v_windows/v/examples/term.ui/event_viewer.v | 47 | ||||
-rw-r--r-- | v_windows/v/examples/term.ui/pong.v | 499 | ||||
-rw-r--r-- | v_windows/v/examples/term.ui/rectangles.v | 97 | ||||
-rw-r--r-- | v_windows/v/examples/term.ui/screenshot_pong.png | bin | 0 -> 1054 bytes | |||
-rw-r--r-- | v_windows/v/examples/term.ui/term_drawing.v | 510 | ||||
-rw-r--r-- | v_windows/v/examples/term.ui/text_editor.v | 583 | ||||
-rw-r--r-- | v_windows/v/examples/term.ui/vyper.v | 475 |
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 Binary files differnew file mode 100644 index 0000000..9127d7b --- /dev/null +++ b/v_windows/v/examples/term.ui/screenshot_pong.png 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() ? +} |