// 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)) } }