termdash/internal/button/button.go

136 lines
4.0 KiB
Go

// 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 a state machine that tracks mouse button clicks.
package button
import (
"image"
"github.com/mum4k/termdash/mouse"
"github.com/mum4k/termdash/terminal/terminalapi"
)
// State represents the state of the mouse button.
type State int
// String implements fmt.Stringer()
func (s State) String() string {
if n, ok := stateNames[s]; ok {
return n
}
return "StateUnknown"
}
// stateNames maps State values to human readable names.
var stateNames = map[State]string{
Up: "StateUp",
Down: "StateDown",
}
const (
// Up is the default idle state of the mouse button.
Up State = iota
// Down is a state where the mouse button is pressed down and held.
Down
)
// FSM implements a finite-state machine that tracks mouse clicks within an
// area.
//
// Simplifies tracking of mouse button clicks, i.e. when the caller wants to
// perform an action only if both the button press and release happen within
// the specified area.
//
// This object is not thread-safe.
type FSM struct {
// button is the mouse button whose state this FSM tracks.
button mouse.Button
// area is the area provided to NewFSM.
area image.Rectangle
// state is the current state of the FSM.
state stateFn
}
// NewFSM creates a new FSM instance that tracks the state of the specified
// mouse button through button events that fall within the provided area.
func NewFSM(button mouse.Button, area image.Rectangle) *FSM {
return &FSM{
button: button,
area: area,
state: wantPress,
}
}
// Event is used to forward mouse events to the state machine.
// Only events related to the button specified on a call to NewFSM are
// processed.
//
// Returns a bool indicating if an action guarded by the button should be
// performed and the state of the button after the provided event.
// The bool is true if the button click should take an effect, i.e. if the
// FSM saw both the button click and its release.
func (fsm *FSM) Event(m *terminalapi.Mouse) (bool, State) {
clicked, bs, next := fsm.state(fsm, m)
fsm.state = next
return clicked, bs
}
// UpdateArea informs FSM of an area change.
// This method is idempotent.
func (fsm *FSM) UpdateArea(area image.Rectangle) {
fsm.area = area
}
// stateFn is a single state in the state machine.
// Returns bool indicating if a click happened, the state of the button and the
// next state of the FSM.
type stateFn func(fsm *FSM, m *terminalapi.Mouse) (bool, State, stateFn)
// wantPress is the initial state, expecting a button press inside the area.
func wantPress(fsm *FSM, m *terminalapi.Mouse) (bool, State, stateFn) {
if m.Button != fsm.button || !m.Position.In(fsm.area) {
return false, Up, wantPress
}
return false, Down, wantRelease
}
// wantRelease waits for a mouse button release in the same area as
// the press.
func wantRelease(fsm *FSM, m *terminalapi.Mouse) (bool, State, stateFn) {
switch m.Button {
case fsm.button:
if m.Position.In(fsm.area) {
// Remain in the same state, since termbox reports move of mouse with
// button held down as a series of clicks, one per position.
return false, Down, wantRelease
}
return false, Up, wantPress
case mouse.ButtonRelease:
if m.Position.In(fsm.area) {
// Seen both press and release, report a click.
return true, Up, wantPress
}
// Release the button even if the release event happened outside of the area.
return false, Up, wantPress
default:
return false, Up, wantPress
}
}