From a265a12bd693b98240e2e6da32667e1dc1ce9dd6 Mon Sep 17 00:00:00 2001 From: Jakub Sobon Date: Sun, 6 May 2018 19:30:20 +0100 Subject: [PATCH] Function that draws a Rectangle. This time for real. --- draw/rectangle.go | 73 ++++++++++++++++++++++++++ draw/rectangle_test.go | 114 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 draw/rectangle.go create mode 100644 draw/rectangle_test.go diff --git a/draw/rectangle.go b/draw/rectangle.go new file mode 100644 index 0000000..547bd76 --- /dev/null +++ b/draw/rectangle.go @@ -0,0 +1,73 @@ +package draw + +// rectangle.go draws a rectangle. + +import ( + "fmt" + "image" + + "github.com/mum4k/termdash/canvas" + "github.com/mum4k/termdash/cell" +) + +// RectangleOption is used to provide options to the Rectangle function. +type RectangleOption interface { + // set sets the provided option. + set(*rectOptions) +} + +// rectOptions stores the provided options. +type rectOptions struct { + cellOpts []cell.Option + char rune +} + +// rectOption implements RectangleOption. +type rectOption func(rOpts *rectOptions) + +// set implements Option.set. +func (ro rectOption) set(rOpts *rectOptions) { + ro(rOpts) +} + +// RectCellOpts sets options on the cells that create the rectangle. +func RectCellOpts(opts ...cell.Option) RectangleOption { + return rectOption(func(rOpts *rectOptions) { + rOpts.cellOpts = append(rOpts.cellOpts, opts...) + }) +} + +// RectangleOption { is the default for the RectChar option. +const DefaultRectChar = ' ' + +// RectChar sets the character used in each of the cells of the rectangle. +func RectChar(c rune) RectangleOption { + return rectOption(func(rOpts *rectOptions) { + rOpts.char = c + }) +} + +// Rectangle draws a filled rectangle on the canvas. +func Rectangle(c *canvas.Canvas, r image.Rectangle, opts ...RectangleOption) error { + opt := &rectOptions{} + for _, o := range opts { + o.set(opt) + } + + if ar := c.Area(); !r.In(ar) { + return fmt.Errorf("the requested rectangle %v doesn't fit the canvas area %v", r, ar) + } + + if r.Dx() < 1 || r.Dy() < 1 { + return fmt.Errorf("the rectangle must be at least 1x1 cell, got %v", r) + } + + for col := r.Min.X; col < r.Max.X; col++ { + for row := r.Min.Y; row < r.Max.Y; row++ { + if err := c.SetCell(image.Point{col, row}, opt.char, opt.cellOpts...); err != nil { + return err + } + } + } + return nil +} diff --git a/draw/rectangle_test.go b/draw/rectangle_test.go new file mode 100644 index 0000000..84ee2a3 --- /dev/null +++ b/draw/rectangle_test.go @@ -0,0 +1,114 @@ +package draw + +import ( + "image" + "testing" + + "github.com/mum4k/termdash/canvas" + "github.com/mum4k/termdash/canvas/testcanvas" + "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/terminal/faketerm" +) + +func TestRectangle(t *testing.T) { + t.Skip() + tests := []struct { + desc string + canvas image.Rectangle + rect image.Rectangle + opts []RectangleOption + want func(size image.Point) *faketerm.Terminal + wantErr bool + }{ + { + desc: "draws a 1x1 rectangle", + canvas: image.Rect(0, 0, 2, 2), + rect: image.Rect(0, 0, 1, 1), + opts: []RectangleOption{ + RectChar('x'), + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "sets cell options", + canvas: image.Rect(0, 0, 2, 2), + rect: image.Rect(0, 0, 1, 1), + opts: []RectangleOption{ + RectChar('x'), + RectCellOpts( + cell.FgColor(cell.ColorBlue), + cell.BgColor(cell.ColorRed), + ), + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "draws a larger rectangle", + canvas: image.Rect(0, 0, 10, 10), + rect: image.Rect(0, 0, 3, 2), + opts: []RectangleOption{ + RectChar('o'), + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + testcanvas.MustApply(c, ft) + return ft + }, + }, + { + desc: "rectangle not in the corner of the canvas", + canvas: image.Rect(0, 0, 10, 10), + rect: image.Rect(1, 1, 9, 3), + opts: []RectangleOption{ + RectChar('o'), + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + c := testcanvas.MustNew(ft.Area()) + testcanvas.MustApply(c, ft) + return ft + }, + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + c, err := canvas.New(tc.canvas) + if err != nil { + t.Fatalf("canvas.New => unexpected error: %v", err) + } + + err = Rectangle(c, tc.rect, tc.opts...) + if (err != nil) != tc.wantErr { + t.Errorf("Rectangle => unexpected error: %v, wantErr: %v", err, tc.wantErr) + } + if err != nil { + return + } + + got, err := faketerm.New(c.Size()) + if err != nil { + t.Fatalf("faketerm.New => unexpected error: %v", err) + } + + if err := c.Apply(got); err != nil { + t.Fatalf("Apply => unexpected error: %v", err) + } + + if diff := faketerm.Diff(tc.want(c.Size()), got); diff != "" { + t.Errorf("Rectangle => %v", diff) + } + }) + } +}