Skip to content

Commit

Permalink
Skeleton of the button widget.
Browse files Browse the repository at this point in the history
  • Loading branch information
mum4k committed Feb 10, 2019
1 parent e9d0471 commit 4caa570
Show file tree
Hide file tree
Showing 4 changed files with 467 additions and 0 deletions.
109 changes: 109 additions & 0 deletions widgets/button/button.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// 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 button implements an interactive widget that can be pressed to
// activate.
package button

import (
"errors"
"image"
"sync"

runewidth "github.com/mattn/go-runewidth"
"github.com/mum4k/termdash/canvas"
"github.com/mum4k/termdash/terminalapi"
"github.com/mum4k/termdash/widgetapi"
)

// CallbackFn is the function called when the button is pressed.
// The callback function must be non-blocking, ideally just storing a value and
// returning, since event processing blocks the redraws.
//
// The callback function must be thread-safe as the mouse or keyboard events
// that press the button are processed in a separate goroutine.
//
// If the function returns an error, the widget will forward it back to the
// termdash infrastructure which causes a panic, unless the user provided a
// termdash.ErrorHandler.
type CallbackFn func() error

// Button can be pressed using a mouse click or a configured keyboard key.
//
// Upon each press, the button invokes a callback provided by the user.
//
// Implements widgetapi.Widget. This object is thread-safe.
type Button struct {
// text in the text label displayed in the button.
text string

// mu protects the widget.
mu sync.Mutex

// opts are the provided options.
opts *options
}

// New returns a new Button that will display the provided text.
// Each press of the button will invoke the callback function.
func New(text string, cFn CallbackFn, opts ...Option) (*Button, error) {
opt := newOptions(runewidth.StringWidth(text))
for _, o := range opts {
o.set(opt)
}
if err := opt.validate(); err != nil {
return nil, err
}
return &Button{
text: text,
opts: opt,
}, nil
}

// Draw draws the Button widget onto the canvas.
// Implements widgetapi.Widget.Draw.
func (b *Button) Draw(cvs *canvas.Canvas) error {
b.mu.Lock()
defer b.mu.Unlock()

return errors.New("unimplemented")
}

// Keyboard processes keyboard events, acts as a button press on the configured
// Key.
//
// Implements widgetapi.Widget.Keyboard.
func (*Button) Keyboard(k *terminalapi.Keyboard) error {
return errors.New("unimplemented")
}

// Mouse processes mouse events, acts as a button press if both the press and
// the release happen inside the button.
//
// Implements widgetapi.Widget.Mouse.
func (*Button) Mouse(m *terminalapi.Mouse) error {
return errors.New("the SegmentDisplay widget doesn't support mouse events")
}

// Options implements widgetapi.Widget.Options.
func (b *Button) Options() widgetapi.Options {
// No need to lock, as the height and width get fixed when New is called.

height := b.opts.height + 1 // One for the shadow.
return widgetapi.Options{
MinimumSize: image.Point{b.opts.width, height},
WantKeyboard: true,
WantMouse: true,
}
}
167 changes: 167 additions & 0 deletions widgets/button/button_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// 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 button

import (
"errors"
"image"
"sync"
"testing"

"github.com/kylelemons/godebug/pretty"
"github.com/mum4k/termdash/canvas"
"github.com/mum4k/termdash/terminal/faketerm"
"github.com/mum4k/termdash/terminalapi"
"github.com/mum4k/termdash/widgetapi"
)

// callbackTracker tracks whether callback was called.
type callbackTracker struct {
// wantErr when set to true, makes callback return an error.
wantErr bool

// called asserts whether the callback was called.
called bool

// count is the number of times the callback was called.
count int

// mu protects the tracker.
mu sync.Mutex
}

// callback is the callback function.
func (ct *callbackTracker) callback() error {
ct.mu.Lock()
defer ct.mu.Unlock()

if ct.wantErr {
return errors.New("ct.wantErr set to true")
}

ct.count++
ct.called = true
return nil
}

func TestButton(t *testing.T) {
tests := []struct {
desc string
text string
opts []Option
events []terminalapi.Event
canvas image.Rectangle
want func(size image.Point) *faketerm.Terminal
wantCallback *callbackTracker
wantNewErr bool
wantDrawErr bool
}{}

for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
gotCallback := &callbackTracker{}
b, err := New(tc.text, gotCallback.callback, tc.opts...)
if (err != nil) != tc.wantNewErr {
t.Errorf("New => unexpected error: %v, wantNewErr: %v", err, tc.wantNewErr)
}
if err != nil {
return
}

for _, ev := range tc.events {
switch ev.(type) {
default:
t.Fatalf("unsupported event type: %T", ev)
}
}

c, err := canvas.New(tc.canvas)
if err != nil {
t.Fatalf("canvas.New => unexpected error: %v", err)
}

err = b.Draw(c)
if (err != nil) != tc.wantDrawErr {
t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
}
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)
}

var want *faketerm.Terminal
if tc.want != nil {
want = tc.want(c.Size())
} else {
want = faketerm.MustNew(c.Size())
}

if diff := faketerm.Diff(want, got); diff != "" {
t.Errorf("Draw => %v", diff)
}

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

func TestKeyboard(t *testing.T) {
ct := &callbackTracker{}
b, err := New("text", ct.callback)
if err != nil {
t.Fatalf("New => unexpected error: %v", err)
}
if err := b.Keyboard(&terminalapi.Keyboard{}); err == nil {
t.Errorf("Keyboard => got nil err, wanted one")
}
}

func TestMouse(t *testing.T) {
ct := &callbackTracker{}
b, err := New("text", ct.callback)
if err != nil {
t.Fatalf("New => unexpected error: %v", err)
}
if err := b.Mouse(&terminalapi.Mouse{}); err == nil {
t.Errorf("Mouse => got nil err, wanted one")
}
}

func TestOptions(t *testing.T) {
ct := &callbackTracker{}
b, err := New("text", ct.callback)
if err != nil {
t.Fatalf("New => unexpected error: %v", err)
}
got := b.Options()
want := widgetapi.Options{
MinimumSize: image.Point{6, 3},
WantKeyboard: true,
WantMouse: true,
}
if diff := pretty.Compare(want, got); diff != "" {
t.Errorf("Options => unexpected diff (-want, +got):\n%s", diff)
}
}
56 changes: 56 additions & 0 deletions widgets/button/buttondemo/buttondemo.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.

// Binary buttondemo shows the functionality of a button widget.
package main

import (
"context"
"time"

"github.com/mum4k/termdash"
"github.com/mum4k/termdash/container"
"github.com/mum4k/termdash/draw"
"github.com/mum4k/termdash/terminal/termbox"
"github.com/mum4k/termdash/terminalapi"
)

func main() {
t, err := termbox.New()
if err != nil {
panic(err)
}
defer t.Close()

ctx, cancel := context.WithCancel(context.Background())

c, err := container.New(
t,
container.Border(draw.LineStyleLight),
container.BorderTitle("PRESS Q TO QUIT"),
)
if err != nil {
panic(err)
}

quitter := func(k *terminalapi.Keyboard) {
if k.Key == 'q' || k.Key == 'Q' {
cancel()
}
}

if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter), termdash.RedrawInterval(1*time.Second)); err != nil {
panic(err)
}
}
Loading

0 comments on commit 4caa570

Please sign in to comment.