Skip to content

Commit

Permalink
Merge pull request #198 from mum4k/dot-display
Browse files Browse the repository at this point in the history
Support displaying the dot character in SegmentDisplay.
  • Loading branch information
mum4k authored May 1, 2019
2 parents f329215 + 426ec9e commit e6a00d5
Show file tree
Hide file tree
Showing 20 changed files with 1,714 additions and 287 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- The `SegmentDisplay` can now display dots and colons ('.' and ':').
- The `Donut` widget now guarantees spacing between the donut and its label.

### Fixed
Expand Down
Binary file modified doc/images/segmentdisplaydemo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
File renamed without changes.
32 changes: 32 additions & 0 deletions internal/area/area.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,35 @@ func ShrinkPercent(area image.Rectangle, topPerc, rightPerc, bottomPerc, leftPer
left := area.Dx() * leftPerc / 100
return Shrink(area, top, right, bottom, left)
}

// MoveUp returns a new area that is moved up by the specified amount of cells.
// Returns an error if the move would result in negative Y coordinates.
// The values must be zero or positive integers.
func MoveUp(area image.Rectangle, cells int) (image.Rectangle, error) {
if min := 0; cells < min {
return image.ZR, fmt.Errorf("cannot move area %v up by %d cells, must be in range %d <= value", area, cells, min)
}

if area.Min.Y < cells {
return image.ZR, fmt.Errorf("cannot move area %v up by %d cells, would result in negative Y coordinate", area, cells)
}

moved := area
moved.Min.Y -= cells
moved.Max.Y -= cells
return moved, nil
}

// MoveDown returns a new area that is moved down by the specified amount of
// cells.
// The values must be zero or positive integers.
func MoveDown(area image.Rectangle, cells int) (image.Rectangle, error) {
if min := 0; cells < min {
return image.ZR, fmt.Errorf("cannot move area %v down by %d cells, must be in range %d <= value", area, cells, min)
}

moved := area
moved.Min.Y += cells
moved.Max.Y += cells
return moved, nil
}
108 changes: 108 additions & 0 deletions internal/area/area_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -871,3 +871,111 @@ func TestShrinkPercent(t *testing.T) {
})
}
}

func TestMoveUp(t *testing.T) {
tests := []struct {
desc string
area image.Rectangle
cells int
want image.Rectangle
wantErr bool
}{
{
desc: "fails on negative cells",
area: image.Rect(0, 0, 1, 1),
cells: -1,
wantErr: true,
},
{
desc: "zero area cannot be moved",
area: image.ZR,
cells: 1,
wantErr: true,
},
{
desc: "cannot move area beyond zero Y coordinate",
area: image.Rect(0, 5, 1, 10),
cells: 6,
wantErr: true,
},
{
desc: "move by zero cells is idempotent",
area: image.Rect(0, 5, 1, 10),
cells: 0,
want: image.Rect(0, 5, 1, 10),
},
{
desc: "moves area up",
area: image.Rect(0, 5, 1, 10),
cells: 3,
want: image.Rect(0, 2, 1, 7),
},
}

for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got, err := MoveUp(tc.area, tc.cells)
if (err != nil) != tc.wantErr {
t.Errorf("MoveUp => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}

if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("MoveUp => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}

func TestMoveDown(t *testing.T) {
tests := []struct {
desc string
area image.Rectangle
cells int
want image.Rectangle
wantErr bool
}{
{
desc: "fails on negative cells",
area: image.Rect(0, 0, 1, 1),
cells: -1,
wantErr: true,
},
{
desc: "moves zero area",
area: image.ZR,
cells: 1,
want: image.Rect(0, 1, 0, 1),
},
{
desc: "move by zero cells is idempotent",
area: image.Rect(0, 5, 1, 10),
cells: 0,
want: image.Rect(0, 5, 1, 10),
},
{
desc: "moves area down",
area: image.Rect(0, 5, 1, 10),
cells: 3,
want: image.Rect(0, 8, 1, 13),
},
}

for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got, err := MoveDown(tc.area, tc.cells)
if (err != nil) != tc.wantErr {
t.Errorf("MoveDown => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}

if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("MoveDown => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}
117 changes: 117 additions & 0 deletions internal/segdisp/dotseg/attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// 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

// attributes.go calculates attributes needed when determining placement of
// segments.

import (
"fmt"
"image"
"math"

"github.com/mum4k/termdash/align"
"github.com/mum4k/termdash/internal/alignfor"
"github.com/mum4k/termdash/internal/area"
"github.com/mum4k/termdash/internal/segdisp"
"github.com/mum4k/termdash/internal/segdisp/sixteen"
)

// attributes contains attributes needed to draw the segment display.
// Refer to doc/segment_placement.svg for a visual aid and explanation of the
// usage of the square roots.
type attributes struct {
// bcAr is the area the attributes were created for.
bcAr image.Rectangle

// segSize is the width of a vertical or height of a horizontal segment.
segSize int

// sixteen are attributes of a 16-segment display when placed on the same
// area.
sixteen *sixteen.Attributes
}

// newAttributes calculates attributes needed to place the segments for the
// provided pixel area.
func newAttributes(bcAr image.Rectangle) *attributes {
segSize := segdisp.SegmentSize(bcAr)
return &attributes{
bcAr: bcAr,
segSize: segSize,
sixteen: sixteen.NewAttributes(bcAr),
}
}

// segArea returns the area for the specified segment.
func (a *attributes) segArea(seg Segment) (image.Rectangle, error) {
// Dots have double width of normal segments to fill more space in the
// segment display.
segSize := a.segSize * 2

// An area representing the dot which gets aligned and moved into position
// below.
dotAr := image.Rect(
a.bcAr.Min.X,
a.bcAr.Min.Y,
a.bcAr.Min.X+segSize,
a.bcAr.Min.Y+segSize,
)
mid, err := alignfor.Rectangle(a.bcAr, dotAr, align.HorizontalCenter, align.VerticalMiddle)
if err != nil {
return image.ZR, err
}

// moveBySize is the multiplier of segment size to determine by how many
// pixels to move D1 and D2 up and down from the center.
const moveBySize = 1.5
moveBy := int(math.Round(moveBySize * float64(segSize)))
switch seg {
case D1:
moved, err := area.MoveUp(mid, moveBy)
if err != nil {
return image.ZR, err
}
return moved, nil

case D2:
moved, err := area.MoveDown(mid, moveBy)
if err != nil {
return image.ZR, err
}
return moved, nil

case D3:
// Align at the middle of the bottom.
bot, err := alignfor.Rectangle(a.bcAr, dotAr, align.HorizontalCenter, align.VerticalBottom)
if err != nil {
return image.ZR, err
}

// Shift up to where the sixteen segment actually places its bottom
// segments.
diff := bot.Min.Y - a.sixteen.VertBotY
// Shift further up by one segment size, since the dots have double width.
diff += a.segSize
moved, err := area.MoveUp(bot, diff)
if err != nil {
return image.ZR, err
}
return moved, nil

default:
return image.ZR, fmt.Errorf("cannot calculate area for %v(%d)", seg, seg)
}
}
56 changes: 56 additions & 0 deletions internal/segdisp/dotseg/attributes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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"
"testing"

"github.com/kylelemons/godebug/pretty"
)

func TestAttributes(t *testing.T) {
tests := []struct {
desc string
brailleAr image.Rectangle
seg Segment
want image.Rectangle
wantErr bool
}{
{
desc: "fails on unsupported segment",
brailleAr: image.Rect(0, 0, 1, 1),
seg: Segment(-1),
wantErr: true,
},
}

for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
attr := newAttributes(tc.brailleAr)
got, err := attr.segArea(tc.seg)
if (err != nil) != tc.wantErr {
t.Errorf("segArea => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}

if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("segArea => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}
Loading

0 comments on commit e6a00d5

Please sign in to comment.