From 0938ae91d87519ededfa09ac5cbc003e8937f274 Mon Sep 17 00:00:00 2001 From: Jakub Sobon Date: Sun, 24 Feb 2019 15:44:13 -0500 Subject: [PATCH] Refactoring internal functions from align to internal/alignfor. --- align/align.go | 103 ------- align/align_test.go | 347 +++------------------- container/container.go | 4 +- internal/alignfor/align.go | 120 ++++++++ internal/alignfor/align_test.go | 351 +++++++++++++++++++++++ internal/draw/border.go | 3 +- widgets/barchart/barchart.go | 3 +- widgets/button/button.go | 3 +- widgets/donut/donut.go | 5 +- widgets/gauge/gauge.go | 4 +- widgets/linechart/internal/axes/label.go | 3 +- widgets/segmentdisplay/segmentdisplay.go | 6 +- 12 files changed, 526 insertions(+), 426 deletions(-) create mode 100644 internal/alignfor/align.go create mode 100644 internal/alignfor/align_test.go diff --git a/align/align.go b/align/align.go index 8505c80..da4087f 100644 --- a/align/align.go +++ b/align/align.go @@ -15,14 +15,6 @@ // Package align defines constants representing types of alignment. package align -import ( - "fmt" - "image" - "strings" - - "github.com/mum4k/termdash/internal/runewidth" -) - // Horizontal indicates the type of horizontal alignment. type Horizontal int @@ -76,98 +68,3 @@ const ( // VerticalBottom is bottom alignment along the vertical axis. VerticalBottom ) - -// hAlign aligns the given area in the rectangle horizontally. -func hAlign(rect image.Rectangle, ar image.Rectangle, h Horizontal) (image.Rectangle, error) { - gap := rect.Dx() - ar.Dx() - switch h { - case HorizontalRight: - // Use gap from above. - case HorizontalCenter: - gap /= 2 - case HorizontalLeft: - gap = 0 - default: - return image.ZR, fmt.Errorf("unsupported horizontal alignment %v", h) - } - - return image.Rect( - rect.Min.X+gap, - ar.Min.Y, - rect.Min.X+gap+ar.Dx(), - ar.Max.Y, - ), nil -} - -// vAlign aligns the given area in the rectangle vertically. -func vAlign(rect image.Rectangle, ar image.Rectangle, v Vertical) (image.Rectangle, error) { - gap := rect.Dy() - ar.Dy() - switch v { - case VerticalBottom: - // Use gap from above. - case VerticalMiddle: - gap /= 2 - case VerticalTop: - gap = 0 - default: - return image.ZR, fmt.Errorf("unsupported vertical alignment %v", v) - } - - return image.Rect( - ar.Min.X, - rect.Min.Y+gap, - ar.Max.X, - rect.Min.Y+gap+ar.Dy(), - ), nil -} - -// Rectangle aligns the area within the rectangle returning the -// aligned area. The area must fall within the rectangle. -func Rectangle(rect image.Rectangle, ar image.Rectangle, h Horizontal, v Vertical) (image.Rectangle, error) { - if !ar.In(rect) { - return image.ZR, fmt.Errorf("cannot align area %v inside rectangle %v, the area falls outside of the rectangle", ar, rect) - } - - aligned, err := hAlign(rect, ar, h) - if err != nil { - return image.ZR, err - } - aligned, err = vAlign(rect, aligned, v) - if err != nil { - return image.ZR, err - } - return aligned, nil -} - -// Text aligns the text within the given rectangle, returns the start point for the text. -// For the purposes of the alignment this assumes that text will be trimmed if -// it overruns the rectangle. -// This only supports a single line of text, the text must not contain newlines. -func Text(rect image.Rectangle, text string, h Horizontal, v Vertical) (image.Point, error) { - if strings.ContainsRune(text, '\n') { - return image.ZP, fmt.Errorf("the provided text contains a newline character: %q", text) - } - - cells := runewidth.StringWidth(text) - var textLen int - if cells < rect.Dx() { - textLen = cells - } else { - textLen = rect.Dx() - } - - textRect := image.Rect( - rect.Min.X, - rect.Min.Y, - // For the purposes of aligning the text, assume that it will be - // trimmed to the available space. - rect.Min.X+textLen, - rect.Min.Y+1, - ) - - aligned, err := Rectangle(rect, textRect, h, v) - if err != nil { - return image.ZP, err - } - return image.Point{aligned.Min.X, aligned.Min.Y}, nil -} diff --git a/align/align_test.go b/align/align_test.go index 44b3d8b..a08c6c6 100644 --- a/align/align_test.go +++ b/align/align_test.go @@ -1,349 +1,76 @@ -// Copyright 2018 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 align -import ( - "image" - "testing" +import "testing" - "github.com/kylelemons/godebug/pretty" -) - -func TestRectangle(t *testing.T) { +func TestHorizontal(t *testing.T) { tests := []struct { - desc string - rect image.Rectangle - area image.Rectangle - hAlign Horizontal - vAlign Vertical - want image.Rectangle - wantErr bool + desc string + align Horizontal + want string }{ { - desc: "area falls outside of the rectangle", - rect: image.Rect(0, 0, 1, 1), - area: image.Rect(1, 1, 2, 2), - hAlign: HorizontalLeft, - vAlign: VerticalTop, - wantErr: true, + desc: "unknown", + align: Horizontal(-1), + want: "HorizontalUnknown", }, { - desc: "unsupported horizontal alignment", - rect: image.Rect(0, 0, 2, 2), - area: image.Rect(0, 0, 1, 1), - hAlign: Horizontal(-1), - vAlign: VerticalTop, - wantErr: true, + desc: "left", + align: HorizontalLeft, + want: "HorizontalLeft", }, { - desc: "unsupported vertical alignment", - rect: image.Rect(0, 0, 2, 2), - area: image.Rect(0, 0, 1, 1), - hAlign: HorizontalLeft, - vAlign: Vertical(-1), - wantErr: true, + desc: "center", + align: HorizontalCenter, + want: "HorizontalCenter", }, { - desc: "nothing to align if the rectangles are equal", - rect: image.Rect(0, 0, 2, 2), - area: image.Rect(0, 0, 2, 2), - hAlign: HorizontalLeft, - vAlign: VerticalTop, - want: image.Rect(0, 0, 2, 2), - }, - { - desc: "aligns top and left, area is zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(0, 0, 1, 1), - hAlign: HorizontalLeft, - vAlign: VerticalTop, - want: image.Rect(0, 0, 1, 1), - }, - { - desc: "aligns top and center, area is zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(0, 0, 1, 1), - hAlign: HorizontalCenter, - vAlign: VerticalTop, - want: image.Rect(1, 0, 2, 1), - }, - { - desc: "aligns top and right, area is zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(0, 0, 1, 1), - hAlign: HorizontalRight, - vAlign: VerticalTop, - want: image.Rect(2, 0, 3, 1), - }, - { - desc: "aligns middle and left, area is zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(0, 0, 1, 1), - hAlign: HorizontalLeft, - vAlign: VerticalMiddle, - want: image.Rect(0, 1, 1, 2), - }, - { - desc: "aligns middle and center, area is zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(0, 0, 1, 1), - hAlign: HorizontalCenter, - vAlign: VerticalMiddle, - want: image.Rect(1, 1, 2, 2), - }, - { - desc: "aligns middle and right, area is zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(0, 0, 1, 1), - hAlign: HorizontalRight, - vAlign: VerticalMiddle, - want: image.Rect(2, 1, 3, 2), - }, - { - desc: "aligns bottom and left, area is zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(0, 0, 1, 1), - hAlign: HorizontalLeft, - vAlign: VerticalBottom, - want: image.Rect(0, 2, 1, 3), - }, - { - desc: "aligns bottom and center, area is zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(0, 0, 1, 1), - hAlign: HorizontalCenter, - vAlign: VerticalBottom, - want: image.Rect(1, 2, 2, 3), - }, - { - desc: "aligns bottom and right, area is zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(0, 0, 1, 1), - hAlign: HorizontalRight, - vAlign: VerticalBottom, - want: image.Rect(2, 2, 3, 3), - }, - { - desc: "aligns top and left, area isn't zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(0, 0, 1, 1), - hAlign: HorizontalLeft, - vAlign: VerticalTop, - want: image.Rect(0, 0, 1, 1), - }, - { - desc: "aligns top and center, area isn't zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(1, 1, 2, 2), - hAlign: HorizontalCenter, - vAlign: VerticalTop, - want: image.Rect(1, 0, 2, 1), - }, - { - desc: "aligns top and right, area isn't zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(1, 1, 2, 2), - hAlign: HorizontalRight, - vAlign: VerticalTop, - want: image.Rect(2, 0, 3, 1), - }, - { - desc: "aligns middle and left, area isn't zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(1, 1, 2, 2), - hAlign: HorizontalLeft, - vAlign: VerticalMiddle, - want: image.Rect(0, 1, 1, 2), - }, - { - desc: "aligns middle and center, area isn't zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(1, 1, 2, 2), - hAlign: HorizontalCenter, - vAlign: VerticalMiddle, - want: image.Rect(1, 1, 2, 2), - }, - { - desc: "aligns middle and right, area isn't zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(1, 1, 2, 2), - hAlign: HorizontalRight, - vAlign: VerticalMiddle, - want: image.Rect(2, 1, 3, 2), - }, - { - desc: "aligns bottom and left, area isn't zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(1, 1, 2, 2), - hAlign: HorizontalLeft, - vAlign: VerticalBottom, - want: image.Rect(0, 2, 1, 3), - }, - { - desc: "aligns bottom and center, area isn't zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(1, 1, 2, 2), - hAlign: HorizontalCenter, - vAlign: VerticalBottom, - want: image.Rect(1, 2, 2, 3), - }, - { - desc: "aligns bottom and right, area isn't zero based", - rect: image.Rect(0, 0, 3, 3), - area: image.Rect(1, 1, 2, 2), - hAlign: HorizontalRight, - vAlign: VerticalBottom, - want: image.Rect(2, 2, 3, 3), + desc: "right", + align: HorizontalRight, + want: "HorizontalRight", }, } for _, tc := range tests { t.Run(tc.desc, func(t *testing.T) { - got, err := Rectangle(tc.rect, tc.area, tc.hAlign, tc.vAlign) - if (err != nil) != tc.wantErr { - t.Errorf("Rectangle => unexpected error: %v, wantErr: %v", err, tc.wantErr) - } - if err != nil { - return - } - - if diff := pretty.Compare(tc.want, got); diff != "" { - t.Errorf("Rectangle => unexpected diff (-want, +got):\n%s", diff) + if got := tc.align.String(); got != tc.want { + t.Errorf("String => %q, want %q", got, tc.want) } }) } } -func TestText(t *testing.T) { +func TestVertical(t *testing.T) { tests := []struct { - desc string - rect image.Rectangle - text string - hAlign Horizontal - vAlign Vertical - want image.Point - wantErr bool + desc string + align Vertical + want string }{ { - desc: "fails when text contains newline", - rect: image.Rect(0, 0, 3, 3), - text: "a\nb", - wantErr: true, + desc: "unknown", + align: Vertical(-1), + want: "VerticalUnknown", }, { - desc: "aligns text top and left", - rect: image.Rect(1, 1, 4, 4), - text: "a", - hAlign: HorizontalLeft, - vAlign: VerticalTop, - want: image.Point{1, 1}, + desc: "top", + align: VerticalTop, + want: "VerticalTop", }, { - desc: "aligns text top and center", - rect: image.Rect(1, 1, 4, 4), - text: "a", - hAlign: HorizontalCenter, - vAlign: VerticalTop, - want: image.Point{2, 1}, + desc: "middle", + align: VerticalMiddle, + want: "VerticalMiddle", }, { - desc: "aligns text top and right", - rect: image.Rect(1, 1, 4, 4), - text: "a", - hAlign: HorizontalRight, - vAlign: VerticalTop, - want: image.Point{3, 1}, - }, - { - desc: "aligns text middle and left", - rect: image.Rect(1, 1, 4, 4), - text: "a", - hAlign: HorizontalLeft, - vAlign: VerticalMiddle, - want: image.Point{1, 2}, - }, - { - desc: "aligns half-width text rune middle and center", - rect: image.Rect(1, 1, 4, 4), - text: "a", - hAlign: HorizontalCenter, - vAlign: VerticalMiddle, - want: image.Point{2, 2}, - }, - { - desc: "aligns full-width text rune middle and center", - rect: image.Rect(1, 1, 4, 4), - text: "界", - hAlign: HorizontalCenter, - vAlign: VerticalMiddle, - want: image.Point{1, 2}, - }, - { - desc: "aligns text middle and right", - rect: image.Rect(1, 1, 4, 4), - text: "a", - hAlign: HorizontalRight, - vAlign: VerticalMiddle, - want: image.Point{3, 2}, - }, - { - desc: "aligns text bottom and left", - rect: image.Rect(1, 1, 4, 4), - text: "a", - hAlign: HorizontalLeft, - vAlign: VerticalBottom, - want: image.Point{1, 3}, - }, - { - desc: "aligns text bottom and center", - rect: image.Rect(1, 1, 4, 4), - text: "a", - hAlign: HorizontalCenter, - vAlign: VerticalBottom, - want: image.Point{2, 3}, - }, - { - desc: "aligns text bottom and right", - rect: image.Rect(1, 1, 4, 4), - text: "a", - hAlign: HorizontalRight, - vAlign: VerticalBottom, - want: image.Point{3, 3}, - }, - { - desc: "aligns text that is too long, assumes trimming", - rect: image.Rect(1, 1, 4, 4), - text: "abcd", - hAlign: HorizontalCenter, - vAlign: VerticalTop, - want: image.Point{1, 1}, + desc: "bottom", + align: VerticalBottom, + want: "VerticalBottom", }, } for _, tc := range tests { t.Run(tc.desc, func(t *testing.T) { - got, err := Text(tc.rect, tc.text, tc.hAlign, tc.vAlign) - if (err != nil) != tc.wantErr { - t.Errorf("Text => unexpected error: %v, wantErr: %v", err, tc.wantErr) - } - if err != nil { - return - } - - if diff := pretty.Compare(tc.want, got); diff != "" { - t.Errorf("Text => unexpected diff (-want, +got):\n%s", diff) + if got := tc.align.String(); got != tc.want { + t.Errorf("String => %q, want %q", got, tc.want) } }) } diff --git a/container/container.go b/container/container.go index 584135b..c45b81a 100644 --- a/container/container.go +++ b/container/container.go @@ -26,7 +26,7 @@ import ( "image" "sync" - "github.com/mum4k/termdash/align" + "github.com/mum4k/termdash/internal/alignfor" "github.com/mum4k/termdash/internal/area" "github.com/mum4k/termdash/internal/event" "github.com/mum4k/termdash/internal/widgetapi" @@ -142,7 +142,7 @@ func (c *Container) widgetArea() (image.Rectangle, error) { if wOpts.Ratio.X > 0 && wOpts.Ratio.Y > 0 { adjusted = area.WithRatio(adjusted, wOpts.Ratio) } - adjusted, err := align.Rectangle(c.usable(), adjusted, c.opts.hAlign, c.opts.vAlign) + adjusted, err := alignfor.Rectangle(c.usable(), adjusted, c.opts.hAlign, c.opts.vAlign) if err != nil { return image.ZR, err } diff --git a/internal/alignfor/align.go b/internal/alignfor/align.go new file mode 100644 index 0000000..d19663a --- /dev/null +++ b/internal/alignfor/align.go @@ -0,0 +1,120 @@ +// Copyright 2018 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 alignfor provides functions that align elements. +package alignfor + +import ( + "fmt" + "image" + "strings" + + "github.com/mum4k/termdash/align" + "github.com/mum4k/termdash/internal/runewidth" +) + +// hAlign aligns the given area in the rectangle horizontally. +func hAlign(rect image.Rectangle, ar image.Rectangle, h align.Horizontal) (image.Rectangle, error) { + gap := rect.Dx() - ar.Dx() + switch h { + case align.HorizontalRight: + // Use gap from above. + case align.HorizontalCenter: + gap /= 2 + case align.HorizontalLeft: + gap = 0 + default: + return image.ZR, fmt.Errorf("unsupported horizontal alignment %v", h) + } + + return image.Rect( + rect.Min.X+gap, + ar.Min.Y, + rect.Min.X+gap+ar.Dx(), + ar.Max.Y, + ), nil +} + +// vAlign aligns the given area in the rectangle vertically. +func vAlign(rect image.Rectangle, ar image.Rectangle, v align.Vertical) (image.Rectangle, error) { + gap := rect.Dy() - ar.Dy() + switch v { + case align.VerticalBottom: + // Use gap from above. + case align.VerticalMiddle: + gap /= 2 + case align.VerticalTop: + gap = 0 + default: + return image.ZR, fmt.Errorf("unsupported vertical alignment %v", v) + } + + return image.Rect( + ar.Min.X, + rect.Min.Y+gap, + ar.Max.X, + rect.Min.Y+gap+ar.Dy(), + ), nil +} + +// Rectangle aligns the area within the rectangle returning the +// aligned area. The area must fall within the rectangle. +func Rectangle(rect image.Rectangle, ar image.Rectangle, h align.Horizontal, v align.Vertical) (image.Rectangle, error) { + if !ar.In(rect) { + return image.ZR, fmt.Errorf("cannot align area %v inside rectangle %v, the area falls outside of the rectangle", ar, rect) + } + + aligned, err := hAlign(rect, ar, h) + if err != nil { + return image.ZR, err + } + aligned, err = vAlign(rect, aligned, v) + if err != nil { + return image.ZR, err + } + return aligned, nil +} + +// Text aligns the text within the given rectangle, returns the start point for the text. +// For the purposes of the alignment this assumes that text will be trimmed if +// it overruns the rectangle. +// This only supports a single line of text, the text must not contain newlines. +func Text(rect image.Rectangle, text string, h align.Horizontal, v align.Vertical) (image.Point, error) { + if strings.ContainsRune(text, '\n') { + return image.ZP, fmt.Errorf("the provided text contains a newline character: %q", text) + } + + cells := runewidth.StringWidth(text) + var textLen int + if cells < rect.Dx() { + textLen = cells + } else { + textLen = rect.Dx() + } + + textRect := image.Rect( + rect.Min.X, + rect.Min.Y, + // For the purposes of aligning the text, assume that it will be + // trimmed to the available space. + rect.Min.X+textLen, + rect.Min.Y+1, + ) + + aligned, err := Rectangle(rect, textRect, h, v) + if err != nil { + return image.ZP, err + } + return image.Point{aligned.Min.X, aligned.Min.Y}, nil +} diff --git a/internal/alignfor/align_test.go b/internal/alignfor/align_test.go new file mode 100644 index 0000000..276fcd1 --- /dev/null +++ b/internal/alignfor/align_test.go @@ -0,0 +1,351 @@ +// Copyright 2018 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 alignfor + +import ( + "image" + "testing" + + "github.com/kylelemons/godebug/pretty" + "github.com/mum4k/termdash/align" +) + +func TestRectangle(t *testing.T) { + tests := []struct { + desc string + rect image.Rectangle + area image.Rectangle + hAlign align.Horizontal + vAlign align.Vertical + want image.Rectangle + wantErr bool + }{ + { + desc: "area falls outside of the rectangle", + rect: image.Rect(0, 0, 1, 1), + area: image.Rect(1, 1, 2, 2), + hAlign: align.HorizontalLeft, + vAlign: align.VerticalTop, + wantErr: true, + }, + { + desc: "unsupported horizontal alignment", + rect: image.Rect(0, 0, 2, 2), + area: image.Rect(0, 0, 1, 1), + hAlign: align.Horizontal(-1), + vAlign: align.VerticalTop, + wantErr: true, + }, + { + desc: "unsupported vertical alignment", + rect: image.Rect(0, 0, 2, 2), + area: image.Rect(0, 0, 1, 1), + hAlign: align.HorizontalLeft, + vAlign: align.Vertical(-1), + wantErr: true, + }, + { + desc: "nothing to align if the rectangles are equal", + rect: image.Rect(0, 0, 2, 2), + area: image.Rect(0, 0, 2, 2), + hAlign: align.HorizontalLeft, + vAlign: align.VerticalTop, + want: image.Rect(0, 0, 2, 2), + }, + { + desc: "aligns top and left, area is zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(0, 0, 1, 1), + hAlign: align.HorizontalLeft, + vAlign: align.VerticalTop, + want: image.Rect(0, 0, 1, 1), + }, + { + desc: "aligns top and center, area is zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(0, 0, 1, 1), + hAlign: align.HorizontalCenter, + vAlign: align.VerticalTop, + want: image.Rect(1, 0, 2, 1), + }, + { + desc: "aligns top and right, area is zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(0, 0, 1, 1), + hAlign: align.HorizontalRight, + vAlign: align.VerticalTop, + want: image.Rect(2, 0, 3, 1), + }, + { + desc: "aligns middle and left, area is zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(0, 0, 1, 1), + hAlign: align.HorizontalLeft, + vAlign: align.VerticalMiddle, + want: image.Rect(0, 1, 1, 2), + }, + { + desc: "aligns middle and center, area is zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(0, 0, 1, 1), + hAlign: align.HorizontalCenter, + vAlign: align.VerticalMiddle, + want: image.Rect(1, 1, 2, 2), + }, + { + desc: "aligns middle and right, area is zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(0, 0, 1, 1), + hAlign: align.HorizontalRight, + vAlign: align.VerticalMiddle, + want: image.Rect(2, 1, 3, 2), + }, + { + desc: "aligns bottom and left, area is zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(0, 0, 1, 1), + hAlign: align.HorizontalLeft, + vAlign: align.VerticalBottom, + want: image.Rect(0, 2, 1, 3), + }, + { + desc: "aligns bottom and center, area is zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(0, 0, 1, 1), + hAlign: align.HorizontalCenter, + vAlign: align.VerticalBottom, + want: image.Rect(1, 2, 2, 3), + }, + { + desc: "aligns bottom and right, area is zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(0, 0, 1, 1), + hAlign: align.HorizontalRight, + vAlign: align.VerticalBottom, + want: image.Rect(2, 2, 3, 3), + }, + { + desc: "aligns top and left, area isn't zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(0, 0, 1, 1), + hAlign: align.HorizontalLeft, + vAlign: align.VerticalTop, + want: image.Rect(0, 0, 1, 1), + }, + { + desc: "aligns top and center, area isn't zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(1, 1, 2, 2), + hAlign: align.HorizontalCenter, + vAlign: align.VerticalTop, + want: image.Rect(1, 0, 2, 1), + }, + { + desc: "aligns top and right, area isn't zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(1, 1, 2, 2), + hAlign: align.HorizontalRight, + vAlign: align.VerticalTop, + want: image.Rect(2, 0, 3, 1), + }, + { + desc: "aligns middle and left, area isn't zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(1, 1, 2, 2), + hAlign: align.HorizontalLeft, + vAlign: align.VerticalMiddle, + want: image.Rect(0, 1, 1, 2), + }, + { + desc: "aligns middle and center, area isn't zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(1, 1, 2, 2), + hAlign: align.HorizontalCenter, + vAlign: align.VerticalMiddle, + want: image.Rect(1, 1, 2, 2), + }, + { + desc: "aligns middle and right, area isn't zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(1, 1, 2, 2), + hAlign: align.HorizontalRight, + vAlign: align.VerticalMiddle, + want: image.Rect(2, 1, 3, 2), + }, + { + desc: "aligns bottom and left, area isn't zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(1, 1, 2, 2), + hAlign: align.HorizontalLeft, + vAlign: align.VerticalBottom, + want: image.Rect(0, 2, 1, 3), + }, + { + desc: "aligns bottom and center, area isn't zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(1, 1, 2, 2), + hAlign: align.HorizontalCenter, + vAlign: align.VerticalBottom, + want: image.Rect(1, 2, 2, 3), + }, + { + desc: "aligns bottom and right, area isn't zero based", + rect: image.Rect(0, 0, 3, 3), + area: image.Rect(1, 1, 2, 2), + hAlign: align.HorizontalRight, + vAlign: align.VerticalBottom, + want: image.Rect(2, 2, 3, 3), + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + got, err := Rectangle(tc.rect, tc.area, tc.hAlign, tc.vAlign) + if (err != nil) != tc.wantErr { + t.Errorf("Rectangle => unexpected error: %v, wantErr: %v", err, tc.wantErr) + } + if err != nil { + return + } + + if diff := pretty.Compare(tc.want, got); diff != "" { + t.Errorf("Rectangle => unexpected diff (-want, +got):\n%s", diff) + } + }) + } +} + +func TestText(t *testing.T) { + tests := []struct { + desc string + rect image.Rectangle + text string + hAlign align.Horizontal + vAlign align.Vertical + want image.Point + wantErr bool + }{ + { + desc: "fails when text contains newline", + rect: image.Rect(0, 0, 3, 3), + text: "a\nb", + wantErr: true, + }, + { + desc: "aligns text top and left", + rect: image.Rect(1, 1, 4, 4), + text: "a", + hAlign: align.HorizontalLeft, + vAlign: align.VerticalTop, + want: image.Point{1, 1}, + }, + { + desc: "aligns text top and center", + rect: image.Rect(1, 1, 4, 4), + text: "a", + hAlign: align.HorizontalCenter, + vAlign: align.VerticalTop, + want: image.Point{2, 1}, + }, + { + desc: "aligns text top and right", + rect: image.Rect(1, 1, 4, 4), + text: "a", + hAlign: align.HorizontalRight, + vAlign: align.VerticalTop, + want: image.Point{3, 1}, + }, + { + desc: "aligns text middle and left", + rect: image.Rect(1, 1, 4, 4), + text: "a", + hAlign: align.HorizontalLeft, + vAlign: align.VerticalMiddle, + want: image.Point{1, 2}, + }, + { + desc: "aligns half-width text rune middle and center", + rect: image.Rect(1, 1, 4, 4), + text: "a", + hAlign: align.HorizontalCenter, + vAlign: align.VerticalMiddle, + want: image.Point{2, 2}, + }, + { + desc: "aligns full-width text rune middle and center", + rect: image.Rect(1, 1, 4, 4), + text: "界", + hAlign: align.HorizontalCenter, + vAlign: align.VerticalMiddle, + want: image.Point{1, 2}, + }, + { + desc: "aligns text middle and right", + rect: image.Rect(1, 1, 4, 4), + text: "a", + hAlign: align.HorizontalRight, + vAlign: align.VerticalMiddle, + want: image.Point{3, 2}, + }, + { + desc: "aligns text bottom and left", + rect: image.Rect(1, 1, 4, 4), + text: "a", + hAlign: align.HorizontalLeft, + vAlign: align.VerticalBottom, + want: image.Point{1, 3}, + }, + { + desc: "aligns text bottom and center", + rect: image.Rect(1, 1, 4, 4), + text: "a", + hAlign: align.HorizontalCenter, + vAlign: align.VerticalBottom, + want: image.Point{2, 3}, + }, + { + desc: "aligns text bottom and right", + rect: image.Rect(1, 1, 4, 4), + text: "a", + hAlign: align.HorizontalRight, + vAlign: align.VerticalBottom, + want: image.Point{3, 3}, + }, + { + desc: "aligns text that is too long, assumes trimming", + rect: image.Rect(1, 1, 4, 4), + text: "abcd", + hAlign: align.HorizontalCenter, + vAlign: align.VerticalTop, + want: image.Point{1, 1}, + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + got, err := Text(tc.rect, tc.text, tc.hAlign, tc.vAlign) + if (err != nil) != tc.wantErr { + t.Errorf("Text => unexpected error: %v, wantErr: %v", err, tc.wantErr) + } + if err != nil { + return + } + + if diff := pretty.Compare(tc.want, got); diff != "" { + t.Errorf("Text => unexpected diff (-want, +got):\n%s", diff) + } + }) + } +} diff --git a/internal/draw/border.go b/internal/draw/border.go index 833bd8c..b97d2c2 100644 --- a/internal/draw/border.go +++ b/internal/draw/border.go @@ -22,6 +22,7 @@ import ( "github.com/mum4k/termdash/align" "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/internal/alignfor" "github.com/mum4k/termdash/internal/canvas" "github.com/mum4k/termdash/linestyle" ) @@ -120,7 +121,7 @@ func drawTitle(c *canvas.Canvas, border image.Rectangle, opt *borderOptions) err border.Max.X-1, // One space for the top right corner char. border.Min.Y+1, ) - start, err := align.Text(available, opt.title, opt.titleHAlign, align.VerticalTop) + start, err := alignfor.Text(available, opt.title, opt.titleHAlign, align.VerticalTop) if err != nil { return err } diff --git a/widgets/barchart/barchart.go b/widgets/barchart/barchart.go index e65e5d1..dd4cbdd 100644 --- a/widgets/barchart/barchart.go +++ b/widgets/barchart/barchart.go @@ -24,6 +24,7 @@ import ( "github.com/mum4k/termdash/align" "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/internal/alignfor" "github.com/mum4k/termdash/internal/area" "github.com/mum4k/termdash/internal/canvas" "github.com/mum4k/termdash/internal/draw" @@ -139,7 +140,7 @@ func (bc *BarChart) drawText(cvs *canvas.Canvas, i int, text string, color cell. barCol = image.Rect(r.Min.X, cvs.Area().Min.Y, r.Max.X, cvs.Area().Max.Y) } - start, err := align.Text(barCol, text, align.HorizontalCenter, align.VerticalBottom) + start, err := alignfor.Text(barCol, text, align.HorizontalCenter, align.VerticalBottom) if err != nil { return err } diff --git a/widgets/button/button.go b/widgets/button/button.go index 8944709..80fe486 100644 --- a/widgets/button/button.go +++ b/widgets/button/button.go @@ -24,6 +24,7 @@ import ( "github.com/mum4k/termdash/align" "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/internal/alignfor" "github.com/mum4k/termdash/internal/button" "github.com/mum4k/termdash/internal/canvas" "github.com/mum4k/termdash/internal/draw" @@ -142,7 +143,7 @@ func (b *Button) Draw(cvs *canvas.Canvas) error { } textAr := image.Rect(buttonAr.Min.X+1, buttonAr.Min.Y, buttonAr.Dx()-1, buttonAr.Max.Y) - start, err := align.Text(textAr, b.text, align.HorizontalCenter, align.VerticalMiddle) + start, err := alignfor.Text(textAr, b.text, align.HorizontalCenter, align.VerticalMiddle) if err != nil { return err } diff --git a/widgets/donut/donut.go b/widgets/donut/donut.go index b88c7eb..254fdc1 100644 --- a/widgets/donut/donut.go +++ b/widgets/donut/donut.go @@ -23,6 +23,7 @@ import ( "sync" "github.com/mum4k/termdash/align" + "github.com/mum4k/termdash/internal/alignfor" "github.com/mum4k/termdash/internal/canvas" "github.com/mum4k/termdash/internal/canvas/braille" "github.com/mum4k/termdash/internal/draw" @@ -173,9 +174,9 @@ func (d *Donut) drawText(cvs *canvas.Canvas, mid image.Point, holeR int) error { } ar := image.Rect(first.X, first.Y, first.X+cells+2, first.Y+1) - start, err := align.Text(ar, t, align.HorizontalCenter, align.VerticalMiddle) + start, err := alignfor.Text(ar, t, align.HorizontalCenter, align.VerticalMiddle) if err != nil { - return fmt.Errorf("align.Text => %v", err) + return fmt.Errorf("alignfor.Text => %v", err) } if err := draw.Text(cvs, t, start, draw.TextMaxX(start.X+needCells), draw.TextCellOpts(d.opts.textCellOpts...)); err != nil { return fmt.Errorf("draw.Text => %v", err) diff --git a/widgets/gauge/gauge.go b/widgets/gauge/gauge.go index d22641d..314fd75 100644 --- a/widgets/gauge/gauge.go +++ b/widgets/gauge/gauge.go @@ -22,8 +22,8 @@ import ( "image" "sync" - "github.com/mum4k/termdash/align" "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/internal/alignfor" "github.com/mum4k/termdash/internal/area" "github.com/mum4k/termdash/internal/canvas" "github.com/mum4k/termdash/internal/draw" @@ -200,7 +200,7 @@ func (g *Gauge) drawText(cvs *canvas.Canvas, progress image.Rectangle) error { return err } - cur, err := align.Text(ar, trimmed, g.opts.hTextAlign, g.opts.vTextAlign) + cur, err := alignfor.Text(ar, trimmed, g.opts.hTextAlign, g.opts.vTextAlign) if err != nil { return err } diff --git a/widgets/linechart/internal/axes/label.go b/widgets/linechart/internal/axes/label.go index c99522a..5e13ba2 100644 --- a/widgets/linechart/internal/axes/label.go +++ b/widgets/linechart/internal/axes/label.go @@ -21,6 +21,7 @@ import ( "image" "github.com/mum4k/termdash/align" + "github.com/mum4k/termdash/internal/alignfor" ) // LabelOrientation represents the orientation of text labels. @@ -115,7 +116,7 @@ func rowLabel(scale *YScale, y int, labelWidth int) (*Label, error) { } ar := rowLabelArea(y, labelWidth) - pos, err := align.Text(ar, v.Text(), align.HorizontalRight, align.VerticalMiddle) + pos, err := alignfor.Text(ar, v.Text(), align.HorizontalRight, align.VerticalMiddle) if err != nil { return nil, fmt.Errorf("unable to align the label value: %v", err) } diff --git a/widgets/segmentdisplay/segmentdisplay.go b/widgets/segmentdisplay/segmentdisplay.go index 97fd735..5a4ad19 100644 --- a/widgets/segmentdisplay/segmentdisplay.go +++ b/widgets/segmentdisplay/segmentdisplay.go @@ -23,7 +23,7 @@ import ( "image" "sync" - "github.com/mum4k/termdash/align" + "github.com/mum4k/termdash/internal/alignfor" "github.com/mum4k/termdash/internal/attrrange" "github.com/mum4k/termdash/internal/canvas" "github.com/mum4k/termdash/internal/segdisp/sixteen" @@ -188,9 +188,9 @@ func (sd *SegmentDisplay) Draw(cvs *canvas.Canvas) error { } text := sd.buff.String() - aligned, err := align.Rectangle(cvs.Area(), segAr.needArea(), sd.opts.hAlign, sd.opts.vAlign) + aligned, err := alignfor.Rectangle(cvs.Area(), segAr.needArea(), sd.opts.hAlign, sd.opts.vAlign) if err != nil { - return fmt.Errorf("align.Rectangle => %v", err) + return fmt.Errorf("alignfor.Rectangle => %v", err) } optRange, err := sd.wOptsTracker.ForPosition(0) // Text options for the current byte.