Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

6 changed files with 80 additions and 128 deletions

View File

@ -1,101 +1,76 @@
package buf2d package buf2d
import ( import (
"fmt"
"strings" "strings"
) )
// Buffer is a 2-dimensional buffer // Buffer is a 2-dimensional rune buffer
type Buffer[T any] struct { type Buffer struct {
data [][]T data [][]fmt.Stringer
x, y int
width int width int
height int height int
parent *Buffer[T] parent *Buffer
StringFunc func(T) string
} }
// NewBuffer makes a new buffer with the given dimensions // NewBuffer makes a new buffer with the given dimensions
func NewBuffer[T any](width, height int, defaultValue T) *Buffer[T] { func NewBuffer(width, height int) *Buffer {
b := make([][]T, height) b := make([][]fmt.Stringer, height)
for y := range b { for y := range b {
b[y] = make([]T, width) b[y] = make([]fmt.Stringer, width)
for x := range b[y] { for x := range b[y] {
b[y][x] = defaultValue b[y][x] = spaceStringer
} }
} }
return &Buffer[T]{ return &Buffer{
x: 0, y: 0,
data: b, data: b,
width: width, width: width,
height: height, height: height,
parent: nil, parent: nil,
StringFunc: MakeDefaultStringFunc[T](),
} }
} }
func (b *Buffer[T]) limX(x int) int { func (b *Buffer) x(x int) int {
return limit(x, 0, b.width-1) return limit(x, 0, b.width-1)
} }
func (b *Buffer[T]) limY(y int) int { func (b *Buffer) y(y int) int {
return limit(y, 0, b.height-1) return limit(y, 0, b.height-1)
} }
// Set sets the value at position (x,y) to c // Set sets the fmt.Stringer at position (x,y) to c
func (b *Buffer[T]) Set(x, y int, v T) { func (b *Buffer) Set(x, y int, c fmt.Stringer) {
if b.width > 0 && b.height > 0 { b.data[b.y(y)][b.x(x)] = c
b.data[b.limY(y)][b.limX(x)] = v
}
} }
// Get returns the value at position (x,y) // SetRune sets the fmt.Stringer at position (x,y) to rn
func (b *Buffer[T]) Get(x, y int) T { func (b *Buffer) SetRune(x, y int, rn rune) {
b.data[b.y(y)][b.x(x)] = newStringerFromRune(rn)
}
// Get returns the fmt.Stringer at position (x,y)
func (b *Buffer) Get(x, y int) fmt.Stringer {
return b.data[y][x] return b.data[y][x]
} }
// Size returns width and height of b // Size returns width and height of b
func (b *Buffer[T]) Size() (w, h int) { func (b *Buffer) Size() (w, h int) {
return b.width, b.height return b.width, b.height
} }
// Width returns the width of b // Width returns the width of b
func (b *Buffer[T]) Width() int { func (b *Buffer) Width() int {
return b.width return b.width
} }
// Height returns the height of b // Height returns the height of b
func (b *Buffer[T]) Height() int { func (b *Buffer) Height() int {
return b.height return b.height
} }
// Offset returns the offset of b relative to its parent buffer. // ForEach calls f for every rune in this buffer
// Offset returns zeros if b has no parent func (b *Buffer) ForEach(f func(x, y int, c fmt.Stringer)) {
func (b *Buffer[T]) Offset() (x, y int) {
return b.x, b.y
}
// OffsetX returns the horizontal offset of b relative to its parent buffer.
// OffsetX returns 0 if b has no parent
func (b *Buffer[T]) OffsetX() int {
return b.x
}
// OffsetY returns the vertical offset of b relative to its parent buffer.
// OffsetY returns 0 if b has no parent
func (b *Buffer[T]) OffsetY() int {
return b.y
}
// ForEachLine calls f for each line in b
func (b *Buffer[T]) ForEachLine(f func(line int, content []T)) {
for line, content := range b.data {
f(line, content)
}
}
// ForEach calls f for every value in b
func (b *Buffer[T]) ForEach(f func(x, y int, v T)) {
for y, col := range b.data { for y, col := range b.data {
for x, v := range col { for x, v := range col {
f(x, y, v) f(x, y, v)
@ -103,11 +78,11 @@ func (b *Buffer[T]) ForEach(f func(x, y int, v T)) {
} }
} }
func (b *Buffer[T]) String() string { func (b *Buffer) String() string {
s := new(strings.Builder) s := new(strings.Builder)
for ci, col := range b.data { for ci, col := range b.data {
for _, v := range col { for _, v := range col {
s.WriteString(b.StringFunc(v)) s.WriteString(fmt.Sprintf("%v", v))
} }
if ci != len(b.data)-1 { if ci != len(b.data)-1 {
s.WriteRune('\n') s.WriteRune('\n')
@ -120,27 +95,25 @@ func (b *Buffer[T]) String() string {
// Modifying the main buffer or the sub buffer will modify the other one as well // Modifying the main buffer or the sub buffer will modify the other one as well
// This method can be used recursively // This method can be used recursively
// If the given dimensions don't fit in the parent buffer, it will be truncated // If the given dimensions don't fit in the parent buffer, it will be truncated
func (b *Buffer[T]) Sub(x, y, w, h int) *Buffer[T] { func (b *Buffer) Sub(x, y, w, h int) *Buffer {
// sanitize inputs // sanitize inputs
x = limit(x, 0, b.width-1) x = limit(x, 0, b.width-1)
y = limit(y, 0, b.height-1) y = limit(y, 0, b.height-1)
w = limit(w, 0, b.width-x) w = limit(w, 1, b.width-x)
h = limit(h, 0, b.height-y) h = limit(h, 1, b.height-y)
// make slice references // make slice references
data := make([][]T, h) data := make([][]fmt.Stringer, h)
for dy := 0; dy < h; dy++ { for dy := 0; dy < h; dy++ {
col := b.data[y+dy] col := b.data[y+dy]
data[dy] = col[x : x+w] data[dy] = col[x : x+w]
} }
// make buffer // make buffer
return &Buffer[T]{ return &Buffer{
x: x, y: y,
data: data, data: data,
width: w, width: w,
height: h, height: h,
parent: b, parent: b,
StringFunc: b.StringFunc,
} }
} }

View File

@ -7,24 +7,13 @@ import (
) )
func TestSub(t *testing.T) { func TestSub(t *testing.T) {
b := NewBuffer(10, 10, ' ') b := NewBuffer(10, 10)
s := b.Sub(1, 1, b.Width()-1, b.Height()-1) s := b.Sub(1, 1, b.Width()-1, b.Height()-1)
b.Set(5, 1, 'a') b.SetRune(5, 1, 'a')
s.Set(5, 5, 'b') s.SetRune(5, 5, 'b')
WriteString(b, "Hello world", 1, 2) b.WriteString("Hello world", 1, 2)
fmt.Println(b) fmt.Println(b)
fmt.Println(strings.Repeat("-", 10)) fmt.Println(strings.Repeat("-", 20))
fmt.Println(s) fmt.Println(s)
} }
func TestSet(t *testing.T) {
b := NewBuffer(0, 0, ' ')
b.Set(0, 0, 'a')
}
func TestOffset(t *testing.T) {
b := NewBuffer(10, 10, ' ')
b = b.Sub(3, 3, 3, 3)
fmt.Println(b.OffsetX(), b.OffsetY())
}

8
go.mod
View File

@ -1,7 +1,3 @@
module git.tordarus.net/tordarus/buf2d module git.tordarus.net/Tordarus/buf2d
go 1.18 go 1.15
require github.com/mattn/go-runewidth v0.0.14
require github.com/rivo/uniseg v0.2.0 // indirect

4
go.sum
View File

@ -1,4 +0,0 @@
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=

View File

@ -1,9 +1,6 @@
package buf2d package buf2d
import ( import "fmt"
"fmt"
"reflect"
)
func limit(v, min, max int) int { func limit(v, min, max int) int {
return getmax(getmin(v, max), min) return getmax(getmin(v, max), min)
@ -23,18 +20,21 @@ func getmin(x, y int) int {
return y return y
} }
func MakeDefaultStringFunc[T any]() func(T) string { type stringerImpl string
if reflect.TypeOf(new(T)).Elem() == reflect.TypeOf(new(rune)).Elem() {
return func(value T) string {
return string((interface{}(value)).(rune))
}
} else if reflect.TypeOf(new(T)).Elem() == reflect.TypeOf(new(string)).Elem() {
return func(value T) string {
return (interface{}(value)).(string)
}
}
return func(value T) string { func (s *stringerImpl) String() string {
return fmt.Sprintf("%v", value) return string(*s)
}
} }
func newStringer(str string) fmt.Stringer {
var impl stringerImpl = stringerImpl(str)
return &impl
}
func newStringerFromRune(rn rune) fmt.Stringer {
return newStringer(string(rn))
}
var (
spaceStringer = newStringer(" ")
)

View File

@ -1,44 +1,42 @@
package buf2d package buf2d
import ( import (
"fmt"
"strings" "strings"
"github.com/mattn/go-runewidth"
) )
// WriteString writes a whole string to the buffer at position (x,y). // WriteString writes a whole string to the buffer at position (x,y)
// no word wrap is applied at all. If the string does not fit, it will be truncated. // no word wrap is applied at all. If the string does not fit, it will be truncated
// It returns the amount of runes in str func (b *Buffer) WriteString(str string, x, y int) {
func WriteString(b *Buffer[rune], str string, x, y int) (width int) {
dx := x dx := x
for _, r := range str { for _, r := range str {
if dx >= b.Width() { if dx >= b.width {
return return
} }
b.Set(dx, y, r) b.Set(dx, y, newStringerFromRune(r))
dx += runewidth.RuneWidth(r) dx++
} }
return dx - x
} }
// WriteMultiLineString writes a multi-line string to the buffer at position (x,y) // WriteMultiLineString writes a multi-line string to the buffer at position (x,y)
// no word wrap is applied at all. If a line does not fit horizontally, it will be truncated // no word wrap is applied at all. If a line does not fit horizontally, it will be truncated
// All lines which do not fit vertically will be truncated as well // All lines which do not fit vertically will be truncated as well
func WriteMultiLineString(b *Buffer[rune], str string, x, y int) { func (b *Buffer) WriteMultiLineString(str string, x, y int) {
lines := strings.Split(str, "\n") lines := strings.Split(str, "\n")
for dy, line := range lines { for dy, line := range lines {
if dy >= b.height { if dy >= b.height {
return return
} }
WriteString(b, line, x, y+dy)
b.WriteString(line, x, y+dy)
} }
} }
// Fill fills the whole buffer with c // Fill fills the whole buffer with c
func (b *Buffer[T]) Fill(value T) { func (b *Buffer) Fill(c fmt.Stringer) {
for _, col := range b.data { for _, col := range b.data {
for ri := range col { for ri := range col {
col[ri] = value col[ri] = c
} }
} }
} }