termdash/private/fakewidget/fakewidget.go

232 lines
7.1 KiB
Go

// 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 fakewidget implements a fake widget that is useful for testing the
// termdash infrastructure.
package fakewidget
import (
"fmt"
"image"
"sync"
"github.com/mum4k/termdash/keyboard"
"github.com/mum4k/termdash/mouse"
"github.com/mum4k/termdash/private/area"
"github.com/mum4k/termdash/private/canvas"
"github.com/mum4k/termdash/private/draw"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
)
// outputLines are the number of lines written by this plugin.
const outputLines = 4
const (
sizeLine = iota
keyboardLine
mouseLine
focusLine
)
// MinimumSize is the minimum size required to draw this widget.
var MinimumSize = image.Point{24, 5}
// Event is an event that should be delivered to the fake widget.
type Event struct {
// Ev is the event to deliver.
Ev terminalapi.Event
// Meta is metadata about the event.
Meta *widgetapi.EventMeta
}
// Mirror is a fake widget. The fake widget draws a border around its assigned
// canvas and writes the size of its assigned canvas on the first line of the
// canvas.
//
// It writes the last received keyboard event onto the second line. It
// writes the last received mouse event onto the third line. If the widget was
// focused at the time of the event, the event will be prepended with a "F:".
//
// If a non-empty string is provided via the Text() method, that text will be
// written right after the canvas size on the first line. If the widget's
// container is focused it writes "focus" onto the fourth line.
//
// The widget requests the same options that are provided to the constructor.
// If the options or canvas size don't allow for the lines mentioned above, the
// widget skips the ones it has no space for.
//
// This is thread-safe and must not be copied.
// Implements widgetapi.Widget.
type Mirror struct {
// lines are the lines that will be drawn on the canvas.
lines []string
// text is the text provided by the last call to Text().
text string
// mu protects lines.
mu sync.RWMutex
// opts options for this widget.
opts widgetapi.Options
}
// New returns a new fake widget.
// The widget will return the provided options on a call to Options().
func New(opts widgetapi.Options) *Mirror {
return &Mirror{
lines: make([]string, outputLines),
opts: opts,
}
}
// Draw draws up to there lines on the canvas, assuming there is space for
// them. Returns an error if the canvas is so small that it cannot even draw a
// 2x2 border on it, or of any of the text lines end up being longer than the
// width of the canvas.
// Draw implements widgetapi.Widget.Draw.
func (mi *Mirror) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
mi.mu.Lock()
defer mi.mu.Unlock()
if meta.Focused {
mi.lines[focusLine] = "focus"
}
if err := cvs.Clear(); err != nil {
return err
}
if err := draw.Border(cvs, cvs.Area()); err != nil {
return err
}
mi.lines[sizeLine] = fmt.Sprintf("%s%s", cvs.Size().String(), mi.text)
usable := area.ExcludeBorder(cvs.Area())
start := cvs.Area().Intersect(usable).Min
for i := 0; i < outputLines; i++ {
if i >= usable.Dy() {
break
}
if err := draw.Text(cvs, mi.lines[i], start, draw.TextMaxX(usable.Max.X)); err != nil {
return err
}
start = image.Point{start.X, start.Y + 1}
}
return nil
}
// Text stores a text that should be displayed right after the canvas size on
// the first line of the output.
func (mi *Mirror) Text(txt string) {
mi.text = txt
}
// Keyboard draws the received key on the canvas.
// Sending the keyboard.KeyEsc causes this widget to forget the last keyboard
// event and return an error instead.
// Keyboard implements widgetapi.Widget.Keyboard.
func (mi *Mirror) Keyboard(k *terminalapi.Keyboard, meta *widgetapi.EventMeta) error {
mi.mu.Lock()
defer mi.mu.Unlock()
if k.Key == keyboard.KeyEsc {
mi.lines[keyboardLine] = ""
return fmt.Errorf("fakewidget received keyboard event: %v", k)
}
if meta.Focused {
mi.lines[keyboardLine] = fmt.Sprintf("F:%s", k.Key.String())
} else {
mi.lines[keyboardLine] = k.Key.String()
}
return nil
}
// Mouse draws the canvas coordinates of the mouse event and the name of the
// received mouse button on the canvas.
// Sending the mouse.ButtonRight causes this widget to forget the last mouse
// event and return an error instead.
// Mouse implements widgetapi.Widget.Mouse.
func (mi *Mirror) Mouse(m *terminalapi.Mouse, meta *widgetapi.EventMeta) error {
mi.mu.Lock()
defer mi.mu.Unlock()
if m.Button == mouse.ButtonRight {
mi.lines[mouseLine] = ""
return fmt.Errorf("fakewidget received mouse event: %v", m)
}
if meta.Focused {
mi.lines[mouseLine] = fmt.Sprintf("F:%v%v", m.Position, m.Button)
} else {
mi.lines[mouseLine] = fmt.Sprintf("%v%v", m.Position, m.Button)
}
return nil
}
// Options implements widgetapi.Widget.Options.
func (mi *Mirror) Options() widgetapi.Options {
return mi.opts
}
// Draw draws the content that would be expected after placing the Mirror
// widget onto the provided canvas and forwarding the given events.
func Draw(t terminalapi.Terminal, cvs *canvas.Canvas, meta *widgetapi.Meta, opts widgetapi.Options, events ...*Event) error {
mirror := New(opts)
return DrawWithMirror(mirror, t, cvs, meta, events...)
}
// MustDraw is like Draw, but panics on all errors.
func MustDraw(t terminalapi.Terminal, cvs *canvas.Canvas, meta *widgetapi.Meta, opts widgetapi.Options, events ...*Event) {
if err := Draw(t, cvs, meta, opts, events...); err != nil {
panic(fmt.Sprintf("Draw => %v", err))
}
}
// DrawWithMirror is like Draw, but uses the provided Mirror instead of creating one.
func DrawWithMirror(mirror *Mirror, t terminalapi.Terminal, cvs *canvas.Canvas, meta *widgetapi.Meta, events ...*Event) error {
for _, ev := range events {
switch e := ev.Ev.(type) {
case *terminalapi.Mouse:
if mirror.opts.WantMouse == widgetapi.MouseScopeNone {
continue
}
if err := mirror.Mouse(e, ev.Meta); err != nil {
return err
}
case *terminalapi.Keyboard:
if mirror.opts.WantKeyboard == widgetapi.KeyScopeNone {
continue
}
if err := mirror.Keyboard(e, ev.Meta); err != nil {
return err
}
default:
return fmt.Errorf("unsupported event type %T", e)
}
}
if err := mirror.Draw(cvs, meta); err != nil {
return err
}
return cvs.Apply(t)
}
// MustDrawWithMirror is like DrawWithMirror, but panics on all errors.
func MustDrawWithMirror(mirror *Mirror, t terminalapi.Terminal, cvs *canvas.Canvas, meta *widgetapi.Meta, events ...*Event) {
if err := DrawWithMirror(mirror, t, cvs, meta, events...); err != nil {
panic(fmt.Sprintf("DrawWithMirror => %v", err))
}
}