diff --git a/canvas/canvas.go b/canvas/canvas.go index 93e3ad2..f7bf0ae 100644 --- a/canvas/canvas.go +++ b/canvas/canvas.go @@ -21,6 +21,7 @@ import ( "github.com/mum4k/termdash/area" "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/cell/runewidth" "github.com/mum4k/termdash/terminalapi" ) @@ -117,6 +118,32 @@ func (c *Canvas) SetCellOpts(p image.Point, opts ...cell.Option) error { return nil } +// SetAreaCells is like SetCell, but sets the specified rune and options on all +// the cells within the provided area. +// This method is idempotent. +func (c *Canvas) SetAreaCells(cellArea image.Rectangle, r rune, opts ...cell.Option) error { + haveArea := c.Area() + if !cellArea.In(haveArea) { + return fmt.Errorf("unable to set cell runes in area %v, it must fit inside the available cell area is %v", cellArea, haveArea) + } + + rw := runewidth.RuneWidth(r) + for row := cellArea.Min.Y; row < cellArea.Max.Y; row++ { + for col := cellArea.Min.X; col < cellArea.Max.X; { + p := image.Point{col, row} + if col+rw > cellArea.Max.X { + break + } + cells, err := c.SetCell(p, r, opts...) + if err != nil { + return err + } + col += cells + } + } + return nil +} + // SetAreaCellOpts is like SetCellOpts, but sets the specified options on all // the cells within the provided area. func (c *Canvas) SetAreaCellOpts(cellArea image.Rectangle, opts ...cell.Option) error { diff --git a/canvas/canvas_test.go b/canvas/canvas_test.go index 62b3eba..be44f53 100644 --- a/canvas/canvas_test.go +++ b/canvas/canvas_test.go @@ -317,6 +317,173 @@ func TestCanvas(t *testing.T) { } } + if err := cvs.Apply(ft); err != nil { + return nil, err + } + return ft, nil + }, + }, + { + desc: "SetAreaCells sets cells in the full canvas", + canvas: image.Rect(0, 0, 1, 1), + ops: func(cvs *Canvas) error { + return cvs.SetAreaCells(image.Rect(0, 0, 1, 1), 'r') + }, + want: func(size image.Point) (*faketerm.Terminal, error) { + ft := faketerm.MustNew(size) + cvs, err := New(ft.Area()) + if err != nil { + return nil, err + } + + if _, err := cvs.SetCell(image.Point{0, 0}, 'r'); err != nil { + return nil, err + } + + if err := cvs.Apply(ft); err != nil { + return nil, err + } + return ft, nil + }, + }, + { + desc: "SetAreaCells is idempotent", + canvas: image.Rect(0, 0, 1, 1), + ops: func(cvs *Canvas) error { + if err := cvs.SetAreaCells(image.Rect(0, 0, 1, 1), 'r'); err != nil { + return err + } + return cvs.SetAreaCells(image.Rect(0, 0, 1, 1), 'r') + }, + want: func(size image.Point) (*faketerm.Terminal, error) { + ft := faketerm.MustNew(size) + cvs, err := New(ft.Area()) + if err != nil { + return nil, err + } + + if _, err := cvs.SetCell(image.Point{0, 0}, 'r'); err != nil { + return nil, err + } + + if err := cvs.Apply(ft); err != nil { + return nil, err + } + return ft, nil + }, + }, + { + desc: "SetAreaCells fails on area too large", + canvas: image.Rect(0, 0, 1, 1), + ops: func(cvs *Canvas) error { + return cvs.SetAreaCells(image.Rect(0, 0, 2, 2), 'r', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)) + }, + wantErr: true, + }, + { + desc: "SetAreaCells sets cell options", + canvas: image.Rect(0, 0, 1, 1), + ops: func(cvs *Canvas) error { + return cvs.SetAreaCells(image.Rect(0, 0, 1, 1), 'r', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)) + }, + want: func(size image.Point) (*faketerm.Terminal, error) { + ft := faketerm.MustNew(size) + cvs, err := New(ft.Area()) + if err != nil { + return nil, err + } + + if _, err := cvs.SetCell(image.Point{0, 0}, 'r', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil { + return nil, err + } + + if err := cvs.Apply(ft); err != nil { + return nil, err + } + return ft, nil + }, + }, + { + desc: "SetAreaCells sets cell in a sub-area", + canvas: image.Rect(0, 0, 3, 3), + ops: func(cvs *Canvas) error { + return cvs.SetAreaCells(image.Rect(0, 0, 2, 2), 'p') + }, + want: func(size image.Point) (*faketerm.Terminal, error) { + ft := faketerm.MustNew(size) + cvs, err := New(ft.Area()) + if err != nil { + return nil, err + } + + for _, p := range []image.Point{ + {0, 0}, + {0, 1}, + {1, 0}, + {1, 1}, + } { + if _, err := cvs.SetCell(p, 'p'); err != nil { + return nil, err + } + } + + if err := cvs.Apply(ft); err != nil { + return nil, err + } + return ft, nil + }, + }, + { + desc: "SetAreaCells sets full-width runes that fit", + canvas: image.Rect(0, 0, 3, 3), + ops: func(cvs *Canvas) error { + return cvs.SetAreaCells(image.Rect(0, 0, 2, 2), '世') + }, + want: func(size image.Point) (*faketerm.Terminal, error) { + ft := faketerm.MustNew(size) + cvs, err := New(ft.Area()) + if err != nil { + return nil, err + } + + for _, p := range []image.Point{ + {0, 0}, + {0, 1}, + } { + if _, err := cvs.SetCell(p, '世'); err != nil { + return nil, err + } + } + + if err := cvs.Apply(ft); err != nil { + return nil, err + } + return ft, nil + }, + }, + { + desc: "SetAreaCells sets full-width runes that will leave a gap at the end of each row", + canvas: image.Rect(0, 0, 3, 3), + ops: func(cvs *Canvas) error { + return cvs.SetAreaCells(image.Rect(0, 0, 3, 3), '世') + }, + want: func(size image.Point) (*faketerm.Terminal, error) { + ft := faketerm.MustNew(size) + cvs, err := New(ft.Area()) + if err != nil { + return nil, err + } + + for _, p := range []image.Point{ + {0, 0}, + {0, 1}, + {0, 2}, + } { + if _, err := cvs.SetCell(p, '世'); err != nil { + return nil, err + } + } + if err := cvs.Apply(ft); err != nil { return nil, err }