Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1 @@
|
||||
<img src='https://raw.githubusercontent.com/vlang/v/master/examples/term.ui/screenshot_pong.png' width=300>
|
||||
@@ -0,0 +1,98 @@
|
||||
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 = unsafe { nil }
|
||||
points []Point
|
||||
color tui.Color = colors[0]
|
||||
color_idx int
|
||||
cut_rate f64 = 5
|
||||
}
|
||||
|
||||
fn frame(mut app App) {
|
||||
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, mut app App) {
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
|
||||
type EventFn = fn (&tui.Event, voidptr)
|
||||
|
||||
type FrameFn = fn (voidptr)
|
||||
|
||||
fn main() {
|
||||
mut app := &App{}
|
||||
app.tui = tui.init(
|
||||
user_data: app
|
||||
frame_fn: FrameFn(frame)
|
||||
event_fn: EventFn(event)
|
||||
hide_cursor: true
|
||||
)
|
||||
app.tui.run()!
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import term.ui as tui
|
||||
|
||||
struct App {
|
||||
mut:
|
||||
tui &tui.Context = unsafe { nil }
|
||||
}
|
||||
|
||||
fn (mut app App) show_header() {
|
||||
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.flush()
|
||||
}
|
||||
|
||||
fn event(e &tui.Event, mut app App) {
|
||||
app.show_header()
|
||||
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 {
|
||||
println('\nGood bye.')
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
type EventFn = fn (&tui.Event, voidptr)
|
||||
|
||||
fn main() {
|
||||
mut app := &App{}
|
||||
app.tui = tui.init(
|
||||
user_data: app
|
||||
event_fn: EventFn(event)
|
||||
window_title: 'V term.ui event viewer'
|
||||
hide_cursor: true
|
||||
capture_events: true
|
||||
frame_rate: 60
|
||||
use_alternate_buffer: false
|
||||
)
|
||||
app.show_header()
|
||||
app.tui.run()!
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
// 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
|
||||
|
||||
const player_two = 0 // Take over this AI controller
|
||||
|
||||
const white = ui.Color{255, 255, 255}
|
||||
|
||||
@[heap]
|
||||
struct App {
|
||||
mut:
|
||||
tui &ui.Context = unsafe { nil }
|
||||
mode Mode = Mode.menu
|
||||
width int
|
||||
height int
|
||||
game &Game = unsafe { nil }
|
||||
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 = unsafe { nil }
|
||||
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 - 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 = unsafe { nil }
|
||||
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 { f32(3) } else { f32(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.0
|
||||
}
|
||||
if p.pos.y + p.racket_size >= g.app.height {
|
||||
p.pos.y = f32(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(mut app App) {
|
||||
app.init()
|
||||
}
|
||||
|
||||
fn frame(mut app App) {
|
||||
app.frame()
|
||||
}
|
||||
|
||||
fn cleanup(mut app App) {
|
||||
unsafe {
|
||||
app.free()
|
||||
}
|
||||
}
|
||||
|
||||
fn fail(error string) {
|
||||
eprintln(error)
|
||||
}
|
||||
|
||||
fn event(e &ui.Event, mut app App) {
|
||||
app.event(e)
|
||||
}
|
||||
|
||||
type InitFn = fn (voidptr)
|
||||
|
||||
type EventFn = fn (&ui.Event, voidptr)
|
||||
|
||||
type FrameFn = fn (voidptr)
|
||||
|
||||
type CleanupFn = fn (voidptr)
|
||||
|
||||
fn main() {
|
||||
mut app := &App{}
|
||||
app.tui = ui.init(
|
||||
user_data: app
|
||||
init_fn: InitFn(init)
|
||||
frame_fn: FrameFn(frame)
|
||||
cleanup_fn: CleanupFn(cleanup)
|
||||
event_fn: EventFn(event)
|
||||
fail_fn: fail
|
||||
capture_events: true
|
||||
hide_cursor: true
|
||||
frame_rate: 60
|
||||
)
|
||||
app.tui.run()!
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
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 = unsafe { nil }
|
||||
rects []Rect
|
||||
cur_rect Rect
|
||||
is_drag bool
|
||||
redraw bool
|
||||
}
|
||||
|
||||
fn random_color() tui.Color {
|
||||
return tui.Color{
|
||||
r: rand.u8()
|
||||
g: rand.u8()
|
||||
b: rand.u8()
|
||||
}
|
||||
}
|
||||
|
||||
fn event(e &tui.Event, mut app App) {
|
||||
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(mut app App) {
|
||||
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
|
||||
}
|
||||
|
||||
type EventFn = fn (&tui.Event, voidptr)
|
||||
|
||||
type FrameFn = fn (voidptr)
|
||||
|
||||
fn main() {
|
||||
mut app := &App{}
|
||||
app.tui = tui.init(
|
||||
user_data: app
|
||||
event_fn: EventFn(event)
|
||||
frame_fn: FrameFn(frame)
|
||||
hide_cursor: true
|
||||
frame_rate: 60
|
||||
)
|
||||
app.tui.run()!
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,508 @@
|
||||
// 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
|
||||
|
||||
const w = 200
|
||||
const h = 100
|
||||
const space = ' '
|
||||
const spaces = ' '
|
||||
const select_color = 'Select color: '
|
||||
const select_size = 'Size: + -'
|
||||
|
||||
struct App {
|
||||
mut:
|
||||
ui &ui.Context = unsafe { nil }
|
||||
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
|
||||
}
|
||||
|
||||
type EventFn = fn (&ui.Event, voidptr)
|
||||
|
||||
type FrameFn = fn (voidptr)
|
||||
|
||||
fn main() {
|
||||
mut app := &App{}
|
||||
app.ui = ui.init(
|
||||
user_data: app
|
||||
frame_fn: FrameFn(frame)
|
||||
event_fn: EventFn(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(mut app App) {
|
||||
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, mut app App) {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,654 @@
|
||||
// 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 functionality is missing compared to your favourite editor :)
|
||||
import strings
|
||||
import os
|
||||
import math
|
||||
import term.ui as tui
|
||||
import encoding.utf8
|
||||
import encoding.utf8.east_asian
|
||||
|
||||
const rune_digits = [`0`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9`]
|
||||
|
||||
const zero_width_unicode = [
|
||||
`\u034f`, // U+034F COMBINING GRAPHEME JOINER
|
||||
`\u061c`, // U+061C ARABIC LETTER MARK
|
||||
`\u17b4`, // U+17B4 KHMER VOWEL INHERENT AQ
|
||||
`\u17b5`, // U+17B5 KHMER VOWEL INHERENT AA
|
||||
`\u200a`, // U+200A HAIR SPACE
|
||||
`\u200b`, // U+200B ZERO WIDTH SPACE
|
||||
`\u200c`, // U+200C ZERO WIDTH NON-JOINER
|
||||
`\u200d`, // U+200D ZERO WIDTH JOINER
|
||||
`\u200e`, // U+200E LEFT-TO-RIGHT MARK
|
||||
`\u200f`, // U+200F RIGHT-TO-LEFT MARK
|
||||
`\u2060`, // U+2060 WORD JOINER
|
||||
`\u2061`, // U+2061 FUNCTION APPLICATION
|
||||
`\u2062`, // U+2062 INVISIBLE TIMES
|
||||
`\u2063`, // U+2063 INVISIBLE SEPARATOR
|
||||
`\u2064`, // U+2064 INVISIBLE PLUS
|
||||
`\u206a`, // U+206A INHIBIT SYMMETRIC SWAPPING
|
||||
`\u206b`, // U+206B ACTIVATE SYMMETRIC SWAPPING
|
||||
`\u206c`, // U+206C INHIBIT ARABIC FORM SHAPING
|
||||
`\u206d`, // U+206D ACTIVATE ARABIC FORM SHAPING
|
||||
`\u206e`, // U+206E NATIONAL DIGIT SHAPES
|
||||
`\u206f`, // U+206F NOMINAL DIGIT SHAPES
|
||||
`\ufeff`, // U+FEFF ZERO WIDTH NO-BREAK SPACE
|
||||
]
|
||||
|
||||
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 = unsafe { nil }
|
||||
ed &Buffer = unsafe { nil }
|
||||
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().runes()
|
||||
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) cur_slice() string {
|
||||
line := b.line(b.cursor.pos_y).runes()
|
||||
if b.cursor.pos_x == 0 || b.cursor.pos_x > line.len {
|
||||
return ''
|
||||
}
|
||||
return line[..b.cursor.pos_x].string()
|
||||
}
|
||||
|
||||
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.runes().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].runes()
|
||||
l, r := line[..x].string(), line[x..].string()
|
||||
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].runes()
|
||||
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.runes().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().runes().len && y >= b.lines.len - 1 {
|
||||
return ''
|
||||
}
|
||||
mut removed := ''
|
||||
if amount < 0 { // backspace (backward)
|
||||
i := b.cursor_index()
|
||||
raw_runes := b.raw().runes()
|
||||
removed = raw_runes[i + amount..i].string()
|
||||
mut left := amount * -1
|
||||
for li := y; li >= 0 && left > 0; li-- {
|
||||
ln := b.lines[li].runes()
|
||||
if left == ln.len + 1 { // All of the line + 1 - since we're going backwards the "+1" is the line break delimiter.
|
||||
b.lines.delete(li)
|
||||
left = 0
|
||||
if y == 0 {
|
||||
return ''
|
||||
}
|
||||
line_above := b.lines[li - 1].runes()
|
||||
b.cursor.pos_x = line_above.len
|
||||
b.cursor.pos_y--
|
||||
break
|
||||
} else 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].runes()
|
||||
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].runes()
|
||||
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.string() + ln.string()
|
||||
b.lines.delete(li)
|
||||
b.cursor.pos_y--
|
||||
b.cursor.pos_x = line_above.len
|
||||
}
|
||||
} else if x == 1 {
|
||||
runes := b.lines[li].runes()
|
||||
b.lines[li] = runes[left..].string()
|
||||
b.cursor.pos_x = 0
|
||||
} else {
|
||||
b.lines[li] = ln[..x - left].string() + ln[x..].string()
|
||||
b.cursor.pos_x -= left
|
||||
}
|
||||
left = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
} else { // delete (forward)
|
||||
i := b.cursor_index() + 1
|
||||
raw_buffer := b.raw().runes()
|
||||
from_i := i
|
||||
mut to_i := i + amount
|
||||
|
||||
if to_i > raw_buffer.len {
|
||||
to_i = raw_buffer.len
|
||||
}
|
||||
removed = raw_buffer[from_i..to_i].string()
|
||||
mut left := amount
|
||||
for li := y; li >= 0 && left > 0; li++ {
|
||||
ln := b.lines[li].runes()
|
||||
if x == ln.len { // at line end
|
||||
if y + 1 <= b.lines.len {
|
||||
b.lines[li] = ln.string() + 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].string() + ln[x + left..].string()
|
||||
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().runes()
|
||||
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().runes()
|
||||
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 := math.min(b.cursor.pos_y, amount)
|
||||
b.move_updown(-dlines)
|
||||
}
|
||||
.page_down {
|
||||
dlines := math.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).runes().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().runes()
|
||||
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).runes()
|
||||
x = line.len
|
||||
} else if x + a >= line.len && y + 1 < b.lines.len {
|
||||
y++
|
||||
line = b.line(b.cursor.pos_y + 1).runes()
|
||||
x = 0
|
||||
}
|
||||
// first, move past all non-`a-zA-Z0-9_` characters
|
||||
for x + a >= 0 && x + a < line.len && !(utf8.is_letter(line[x + a])
|
||||
|| line[x + a] in rune_digits || line[x + a] == `_`) {
|
||||
x += a
|
||||
}
|
||||
// then, move past all the letters and numbers
|
||||
for x + a >= 0 && x + a < line.len && (utf8.is_letter(line[x + a])
|
||||
|| line[x + a] in rune_digits || 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(mut app App) {
|
||||
app.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().runes().len {
|
||||
move_x := a.magnet_x - buffer.cursor.pos_x
|
||||
buffer.move_cursor(move_x, .right)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn frame(mut a App) {
|
||||
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()
|
||||
|
||||
// Unicode: Handle correct mapping of cursor X position in terminal.
|
||||
mut ch_x := view.cursor.pos_x
|
||||
mut sl := ed.cur_slice().replace('\t', ' '.repeat(ed.tab_width))
|
||||
if sl.len > 0 {
|
||||
// Strip out any zero-width codepoints.
|
||||
sl = sl.runes().filter(it !in zero_width_unicode).string()
|
||||
ch_x = east_asian.display_width(sl, 1)
|
||||
}
|
||||
|
||||
a.tui.set_cursor_position(ch_x + 1, ed.cursor.pos_y + 1 - a.viewport)
|
||||
a.tui.flush()
|
||||
}
|
||||
|
||||
fn event(e &tui.Event, mut a App) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
} else if e.typ == .mouse_scroll {
|
||||
direction := if e.direction == .up { Movement.down } else { Movement.up }
|
||||
buffer.move_cursor(1, direction)
|
||||
}
|
||||
}
|
||||
|
||||
type InitFn = fn (voidptr)
|
||||
|
||||
type EventFn = fn (&tui.Event, voidptr)
|
||||
|
||||
type FrameFn = fn (voidptr)
|
||||
|
||||
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: InitFn(init)
|
||||
frame_fn: FrameFn(frame)
|
||||
event_fn: EventFn(event)
|
||||
capture_events: true
|
||||
)
|
||||
a.tui.run()!
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
// import modules for use in app
|
||||
import term.ui as termui
|
||||
import rand
|
||||
import math.vec
|
||||
|
||||
// define some global constants
|
||||
const block_size = 1
|
||||
const buffer = 10
|
||||
const green = termui.Color{0, 255, 0}
|
||||
const grey = termui.Color{150, 150, 150}
|
||||
const white = termui.Color{255, 255, 255}
|
||||
const blue = termui.Color{0, 0, 255}
|
||||
const red = termui.Color{255, 0, 0}
|
||||
const 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
|
||||
type Vec = vec.Vec2[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) or { min_x }
|
||||
v.y = rand.int_in_range(min_y, max_y) or { min_y }
|
||||
}
|
||||
|
||||
// part of snake's body representation
|
||||
struct BodyPart {
|
||||
mut:
|
||||
pos Vec = Vec{block_size, block_size}
|
||||
color termui.Color = green
|
||||
facing Orientation = .top
|
||||
}
|
||||
|
||||
// snake representation
|
||||
struct Snake {
|
||||
mut:
|
||||
app &App = unsafe { nil }
|
||||
direction Orientation
|
||||
body []BodyPart
|
||||
velocity Vec
|
||||
}
|
||||
|
||||
// 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 vel := Vec{}
|
||||
match direction {
|
||||
.top {
|
||||
vel.x = 0
|
||||
vel.y = -1 * block_size
|
||||
}
|
||||
.right {
|
||||
vel.x = 2 * block_size
|
||||
vel.y = 0
|
||||
}
|
||||
.bottom {
|
||||
vel.x = 0
|
||||
vel.y = block_size
|
||||
}
|
||||
.left {
|
||||
vel.x = -2 * block_size
|
||||
vel.y = 0
|
||||
}
|
||||
}
|
||||
|
||||
s.direction = direction
|
||||
s.velocity = vel
|
||||
}
|
||||
|
||||
// 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) or { 0 }
|
||||
s.velocity.x = speeds[rand.intn(speeds.len) or { 0 }]
|
||||
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{block_size, block_size}
|
||||
captured bool
|
||||
color termui.Color = grey
|
||||
app &App = unsafe { nil }
|
||||
}
|
||||
|
||||
// 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 = unsafe { nil }
|
||||
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(mut app App) {
|
||||
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, mut app App) {
|
||||
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(mut app App) {
|
||||
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{-1, -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,
|
||||
' ##### # # # # ###### ####### ## ###### # # ')
|
||||
}
|
||||
|
||||
type InitFn = fn (voidptr)
|
||||
|
||||
type EventFn = fn (&termui.Event, voidptr)
|
||||
|
||||
type FrameFn = fn (voidptr)
|
||||
|
||||
fn main() {
|
||||
mut app := &App{}
|
||||
app.termui = termui.init(
|
||||
user_data: app
|
||||
event_fn: EventFn(event)
|
||||
frame_fn: FrameFn(frame)
|
||||
init_fn: InitFn(init)
|
||||
hide_cursor: true
|
||||
frame_rate: 10
|
||||
)
|
||||
app.termui.run()!
|
||||
}
|
||||
Reference in new issue
Block a user