diff --git a/internal/segdisp/dotseg/dotseg.go b/internal/segdisp/dotseg/dotseg.go new file mode 100644 index 0000000..1d1d91d --- /dev/null +++ b/internal/segdisp/dotseg/dotseg.go @@ -0,0 +1,223 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Package dotseg simulates a segment display that can draw dots. + +Given a canvas, determines the placement and size of the individual +segments and exposes API that can turn individual segments on and off or +display dot characters. + +The following outlines segments in the display and their names. + + --------------- + | | + | | + | | + | o D1 | + | | + | | + | | + | o D2 | + | | + | | + | o D3 | + --------------- +*/ +package dotseg + +import ( + "fmt" + "strings" + + "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/internal/canvas" + "github.com/mum4k/termdash/internal/segdisp" +) + +// Segment represents a single segment in the display. +type Segment int + +// String implements fmt.Stringer() +func (s Segment) String() string { + if n, ok := segmentNames[s]; ok { + return n + } + return "SegmentUnknown" +} + +// segmentNames maps Segment values to human readable names. +var segmentNames = map[Segment]string{ + D1: "D1", + D2: "D2", + D3: "D3", +} + +const ( + segmentUnknown Segment = iota + + // D1 is a segment, see the diagram above. + D1 + // D2 is a segment, see the diagram above. + D2 + // D3 is a segment, see the diagram above. + D3 + + segmentMax // Used for validation. +) + +// characterSegments maps characters that can be displayed on their segments. +var characterSegments = map[rune][]Segment{ + ':': {D1, D2}, + '.': {D3}, +} + +// SupportedChars returns all characters this display supports. +func SupportedChars() string { + var b strings.Builder + for r := range characterSegments { + b.WriteRune(r) + } + return b.String() +} + +// AllSegments returns all segments in an undefined order. +func AllSegments() []Segment { + var res []Segment + for s := range segmentNames { + res = append(res, s) + } + return res +} + +// Option is used to provide options. +type Option interface { + // set sets the provided option. + set(*Display) +} + +// option implements Option. +type option func(*Display) + +// set implements Option.set. +func (o option) set(d *Display) { + o(d) +} + +// CellOpts sets the cell options on the cells that contain the segment display. +func CellOpts(cOpts ...cell.Option) Option { + return option(func(d *Display) { + d.cellOpts = cOpts + }) +} + +// Display represents the segment display. +// This object is not thread-safe. +type Display struct { + // segments maps segments to their current status. + segments map[Segment]bool + + cellOpts []cell.Option +} + +// New creates a new segment display. +// Initially all the segments are off. +func New(opts ...Option) *Display { + d := &Display{ + segments: map[Segment]bool{}, + } + + for _, opt := range opts { + opt.set(d) + } + return d +} + +// Clear clears the entire display, turning all segments off. +func (d *Display) Clear(opts ...Option) { + for _, opt := range opts { + opt.set(d) + } + + d.segments = map[Segment]bool{} +} + +// SetSegment sets the specified segment on. +// This method is idempotent. +func (d *Display) SetSegment(s Segment) error { + if s <= segmentUnknown || s >= segmentMax { + return fmt.Errorf("unknown segment %v(%d)", s, s) + } + d.segments[s] = true + return nil +} + +// ClearSegment sets the specified segment off. +// This method is idempotent. +func (d *Display) ClearSegment(s Segment) error { + if s <= segmentUnknown || s >= segmentMax { + return fmt.Errorf("unknown segment %v(%d)", s, s) + } + d.segments[s] = false + return nil +} + +// ToggleSegment toggles the state of the specified segment, i.e it either sets +// or clears it depending on its current state. +func (d *Display) ToggleSegment(s Segment) error { + if s <= segmentUnknown || s >= segmentMax { + return fmt.Errorf("unknown segment %v(%d)", s, s) + } + if d.segments[s] { + d.segments[s] = false + } else { + d.segments[s] = true + } + return nil +} + +// SetCharacter sets all the segments that are needed to display the provided +// character. +// The display only supports characters returned by SupportedsChars(). +// Doesn't clear the display of segments set previously. +func (d *Display) SetCharacter(c rune) error { + seg, ok := characterSegments[c] + if !ok { + return fmt.Errorf("display doesn't support character %q rune(%v)", c, c) + } + + for _, s := range seg { + if err := d.SetSegment(s); err != nil { + return err + } + } + return nil +} + +// Draw draws the current state of the segment display onto the canvas. +// The canvas must be at least MinCols x MinRows cells, or an error will be +// returned. +// Any options provided to draw overwrite the values provided to New. +func (d *Display) Draw(cvs *canvas.Canvas, opts ...Option) error { + for _, o := range opts { + o.set(d) + } + + bc, _, err := segdisp.ToBraille(cvs) + if err != nil { + return err + } + + return bc.CopyTo(cvs) +} diff --git a/internal/segdisp/dotseg/dotseg_test.go b/internal/segdisp/dotseg/dotseg_test.go new file mode 100644 index 0000000..a9f0bb7 --- /dev/null +++ b/internal/segdisp/dotseg/dotseg_test.go @@ -0,0 +1,296 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dotseg + +import ( + "image" + "sort" + "testing" + + "github.com/kylelemons/godebug/pretty" + "github.com/mum4k/termdash/internal/area" + "github.com/mum4k/termdash/internal/canvas" + "github.com/mum4k/termdash/internal/canvas/testcanvas" + "github.com/mum4k/termdash/internal/faketerm" + "github.com/mum4k/termdash/internal/segdisp" +) + +func TestDraw(t *testing.T) { + tests := []struct { + desc string + opts []Option + drawOpts []Option + cellCanvas image.Rectangle + // If not nil, it is called before Draw is called and can set, clear or + // toggle segments or characters. + update func(*Display) error + want func(size image.Point) *faketerm.Terminal + wantErr bool + wantUpdateErr bool + }{ + { + desc: "fails for area not wide enough", + cellCanvas: image.Rect(0, 0, segdisp.MinCols-1, segdisp.MinRows), + wantErr: true, + }, + { + desc: "fails for area not tall enough", + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows-1), + wantErr: true, + }, + { + desc: "fails to set invalid segment (too small)", + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows), + update: func(d *Display) error { + return d.SetSegment(Segment(-1)) + }, + wantUpdateErr: true, + }, + { + desc: "fails to set invalid segment (too large)", + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows), + update: func(d *Display) error { + return d.SetSegment(Segment(segmentMax)) + }, + wantUpdateErr: true, + }, + { + desc: "fails to clear invalid segment (too small)", + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows), + update: func(d *Display) error { + return d.ClearSegment(Segment(-1)) + }, + wantUpdateErr: true, + }, + { + desc: "fails to clear invalid segment (too large)", + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows), + update: func(d *Display) error { + return d.ClearSegment(Segment(segmentMax)) + }, + wantUpdateErr: true, + }, + { + desc: "fails to toggle invalid segment (too small)", + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows), + update: func(d *Display) error { + return d.ToggleSegment(Segment(-1)) + }, + wantUpdateErr: true, + }, + { + desc: "fails to toggle invalid segment (too large)", + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows), + update: func(d *Display) error { + return d.ToggleSegment(Segment(segmentMax)) + }, + wantUpdateErr: true, + }, + { + desc: "empty when no segments set", + cellCanvas: image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows), + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + d := New(tc.opts...) + if tc.update != nil { + err := tc.update(d) + if (err != nil) != tc.wantUpdateErr { + t.Errorf("tc.update => unexpected error: %v, wantUpdateErr: %v", err, tc.wantUpdateErr) + } + if err != nil { + return + } + } + + cvs, err := canvas.New(tc.cellCanvas) + if err != nil { + t.Fatalf("canvas.New => unexpected error: %v", err) + } + + { + err := d.Draw(cvs, tc.drawOpts...) + if (err != nil) != tc.wantErr { + t.Errorf("Draw => unexpected error: %v, wantErr: %v", err, tc.wantErr) + } + if err != nil { + return + } + } + + size := area.Size(tc.cellCanvas) + want := faketerm.MustNew(size) + if tc.want != nil { + want = tc.want(size) + } + + got, err := faketerm.New(size) + if err != nil { + t.Fatalf("faketerm.New => unexpected error: %v", err) + } + if err := cvs.Apply(got); err != nil { + t.Fatalf("bc.Apply => unexpected error: %v", err) + } + if diff := faketerm.Diff(want, got); diff != "" { + t.Fatalf("Draw => %v", diff) + } + + }) + } +} + +// mustDrawSegments returns a fake terminal of the specified size with the +// segments drawn on it or panics. +func mustDrawSegments(size image.Point, seg ...Segment) *faketerm.Terminal { + ft := faketerm.MustNew(size) + cvs := testcanvas.MustNew(ft.Area()) + + d := New() + for _, s := range seg { + if err := d.SetSegment(s); err != nil { + panic(err) + } + } + + if err := d.Draw(cvs); err != nil { + panic(err) + } + + testcanvas.MustApply(cvs, ft) + return ft +} + +func TestSetCharacter(t *testing.T) { + tests := []struct { + desc string + char rune + // If not nil, it is called before Draw is called and can set, clear or + // toggle segments or characters. + update func(*Display) error + want func(size image.Point) *faketerm.Terminal + wantErr bool + }{ + { + desc: "fails on unsupported character", + char: 'A', + wantErr: true, + }, + { + desc: "doesn't clear the display", + update: func(d *Display) error { + return d.SetSegment(D3) + }, + char: ':', + want: func(size image.Point) *faketerm.Terminal { + return mustDrawSegments(size, D1, D2, D3) + }, + }, + { + desc: "displays '.'", + char: '.', + want: func(size image.Point) *faketerm.Terminal { + return mustDrawSegments(size, D3) + }, + }, + { + desc: "displays ':'", + char: ':', + want: func(size image.Point) *faketerm.Terminal { + return mustDrawSegments(size, D1, D2) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + d := New() + if tc.update != nil { + err := tc.update(d) + if err != nil { + t.Fatalf("tc.update => unexpected error: %v", err) + } + } + + { + err := d.SetCharacter(tc.char) + if (err != nil) != tc.wantErr { + t.Errorf("SetCharacter => unexpected error: %v, wantErr: %v", err, tc.wantErr) + } + if err != nil { + return + } + } + + ar := image.Rect(0, 0, segdisp.MinCols, segdisp.MinRows) + cvs, err := canvas.New(ar) + if err != nil { + t.Fatalf("canvas.New => unexpected error: %v", err) + } + + if err := d.Draw(cvs); err != nil { + t.Fatalf("Draw => unexpected error: %v", err) + } + + size := area.Size(ar) + want := faketerm.MustNew(size) + if tc.want != nil { + want = tc.want(size) + } + + got, err := faketerm.New(size) + if err != nil { + t.Fatalf("faketerm.New => unexpected error: %v", err) + } + if err := cvs.Apply(got); err != nil { + t.Fatalf("bc.Apply => unexpected error: %v", err) + } + if diff := faketerm.Diff(want, got); diff != "" { + t.Fatalf("SetCharacter => %v", diff) + } + }) + } +} + +func TestAllSegments(t *testing.T) { + want := []Segment{D1, D2, D3} + got := AllSegments() + sort.Slice(got, func(i, j int) bool { + return int(got[i]) < int(got[j]) + }) + if diff := pretty.Compare(want, got); diff != "" { + t.Errorf("AllSegments => unexpected diff (-want, +got):\n%s", diff) + } +} + +func TestSupportedsChars(t *testing.T) { + want := []rune{'.', ':'} + + gotStr := SupportedChars() + var got []rune + for _, r := range gotStr { + got = append(got, r) + } + sort.Slice(got, func(i, j int) bool { + return int(got[i]) < int(got[j]) + }) + sort.Slice(want, func(i, j int) bool { + return int(want[i]) < int(want[j]) + }) + if diff := pretty.Compare(want, got); diff != "" { + t.Errorf("SupportedChars => unexpected diff (-want, +got):\n%s", diff) + } +} diff --git a/internal/segdisp/dotseg/testdotseg/testdotseg.go b/internal/segdisp/dotseg/testdotseg/testdotseg.go new file mode 100644 index 0000000..9324f21 --- /dev/null +++ b/internal/segdisp/dotseg/testdotseg/testdotseg.go @@ -0,0 +1,37 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package testdotseg provides helpers for tests that use the dotseg package. +package testdotseg + +import ( + "fmt" + + "github.com/mum4k/termdash/internal/canvas" + "github.com/mum4k/termdash/internal/segdisp/dotseg" +) + +// MustSetCharacter sets the character on the display or panics. +func MustSetCharacter(d *dotseg.Display, c rune) { + if err := d.SetCharacter(c); err != nil { + panic(fmt.Errorf("dotseg.Display.SetCharacter => unexpected error: %v", err)) + } +} + +// MustDraw draws the display onto the canvas or panics. +func MustDraw(d *dotseg.Display, cvs *canvas.Canvas, opts ...dotseg.Option) { + if err := d.Draw(cvs, opts...); err != nil { + panic(fmt.Errorf("dotseg.Display.Draw => unexpected error: %v", err)) + } +}