erm/internal/app/darktile/gui/mouse.go

359 lines
8.1 KiB
Go

package gui
import (
"fmt"
"time"
"github.com/hajimehoshi/ebiten/v2"
"github.com/liamg/darktile/internal/app/darktile/hinters"
"github.com/liamg/darktile/internal/app/darktile/termutil"
)
// time allowed between mouse clicks to chain them into e.g. double-click
const clickChainWindowMS = 500
// max duration of a click before it is counted as a drag
const clickMaxDuration = 100
func (g *GUI) handleMouse() error {
_, scrollY := ebiten.Wheel()
if scrollY < 0 {
g.terminal.GetActiveBuffer().ScrollDown(5)
} else if scrollY > 0 {
g.terminal.GetActiveBuffer().ScrollUp(5)
}
x, y := ebiten.CursorPosition()
col := x / g.fontManager.CharSize().X
line := y / g.fontManager.CharSize().Y
var moved bool
if col != int(g.mousePos.Col) || line != int(g.mousePos.Line) {
if col >= 0 && col < int(g.terminal.GetActiveBuffer().ViewWidth()) && line >= 0 && line < int(g.terminal.GetActiveBuffer().ViewHeight()) {
// mouse moved!
moved = true
g.mousePos = termutil.Position{
Col: uint16(col),
Line: uint64(line),
}
if !ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
if err := g.handleMouseMove(g.mousePos); err != nil {
return err
}
}
} else if err := g.clearHinters(); err != nil {
return err
}
}
pressedLeft := ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) && g.mouseStateLeft != MouseStatePressed
pressedMiddle := ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) && g.mouseStateMiddle != MouseStatePressed
pressedRight := ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) && g.mouseStateRight != MouseStatePressed
released := (!ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) && g.mouseStateLeft == MouseStatePressed) ||
(!ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) && g.mouseStateMiddle == MouseStatePressed) ||
(!ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) && g.mouseStateRight == MouseStatePressed)
defer func() {
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
g.mouseStateLeft = MouseStatePressed
} else {
g.mouseStateLeft = MouseStateNone
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) {
g.mouseStateMiddle = MouseStatePressed
} else {
g.mouseStateMiddle = MouseStateNone
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) {
g.mouseStateRight = MouseStatePressed
} else {
g.mouseStateRight = MouseStateNone
}
}()
if pressedLeft || pressedMiddle || pressedRight || released {
if g.handleMouseRemotely(x, y, pressedLeft, pressedMiddle, pressedRight, released, moved) {
return nil
}
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
if g.mouseStateLeft == MouseStatePressed {
if g.mouseDrag {
// update selection end
g.terminal.GetActiveBuffer().SetSelectionEnd(termutil.Position{
Line: uint64(line),
Col: uint16(col),
})
} else if time.Since(g.lastClick) > time.Millisecond*clickMaxDuration && !ebiten.IsKeyPressed(ebiten.KeyControl) {
g.mouseDrag = true
}
} else {
if g.clickCount == 0 || time.Since(g.lastClick) < time.Millisecond*clickChainWindowMS {
g.clickCount++
} else {
g.clickCount = 1
}
g.lastClick = time.Now()
handled, err := g.handleClick(g.clickCount, x, y)
if err != nil {
return err
}
if handled {
g.mouseDrag = false
return nil
}
//set selection start
col := x / g.fontManager.CharSize().X
line := y / g.fontManager.CharSize().Y
g.terminal.GetActiveBuffer().SetSelectionStart(termutil.Position{
Line: uint64(line),
Col: uint16(col),
})
}
ebiten.ScheduleFrame()
} else {
g.mouseDrag = false
}
return nil
}
func (g *GUI) clearHinters() error {
if g.activeHinter > -1 {
if err := g.hinters[g.activeHinter].Deactivate(g); err != nil {
return err
}
g.activeHinter = -1
}
return nil
}
// mouse moved to cell (not during click + drag)
func (g *GUI) handleMouseMove(pos termutil.Position) error {
// start uses raw coords
start, _, text, index, ok := g.terminal.GetActiveBuffer().GetBoundedTextAtPosition(pos)
if !ok {
g.clearHinters()
return nil
}
activeHinter := -1
for i, hinter := range g.hinters {
if ok, offset, length := hinter.Match(text, index); ok {
match := text[offset : offset+length]
newStartX := int(start.Col) + offset
newStartY := start.Line
for newStartX >= int(g.terminal.GetActiveBuffer().ViewWidth()) {
newStartX -= int(g.terminal.GetActiveBuffer().ViewWidth())
newStartY++
}
newEndX := newStartX + length - 1
newEndY := newStartY
for newEndX > int(g.terminal.GetActiveBuffer().ViewWidth()) {
newEndX -= int(g.terminal.GetActiveBuffer().ViewWidth())
newEndY++
}
matchStart := termutil.Position{
Col: uint16(newStartX),
Line: newStartY,
}
matchEnd := termutil.Position{
Col: uint16(newEndX),
Line: newEndY,
}
if err := hinter.Activate(g, match, matchStart, matchEnd); err != nil {
return err
}
activeHinter = i
break
}
}
// hinter was just deactivated
if g.activeHinter > -1 && activeHinter == -1 {
if err := g.clearHinters(); err != nil {
return err
}
}
g.activeHinter = activeHinter
return nil
}
func WithHinter(h hinters.Hinter) func(g *GUI) error {
return func(g *GUI) error {
g.hinters = append(g.hinters, h)
return nil
}
}
func (g *GUI) handleClick(clickCount, x, y int) (bool, error) {
switch clickCount {
case 1: // single click
if ebiten.IsKeyPressed(ebiten.KeyControl) { // ctrl + click to run hinters
if g.activeHinter > -1 {
g.hinters[g.activeHinter].Click(g)
}
} else {
g.terminal.GetActiveBuffer().ClearSelection()
}
case 2: //double click
col := uint16(x / g.fontManager.CharSize().X)
line := uint64(y / g.fontManager.CharSize().Y)
g.terminal.GetActiveBuffer().SelectWordAt(termutil.Position{Col: col, Line: line}, wordMatcher)
return true, nil
default: // triple click (or more!)
g.terminal.GetActiveBuffer().ExtendSelectionToEntireLines()
return true, nil
}
return false, nil
}
func alphaMatcher(r rune) bool {
if r >= 65 && r <= 90 {
return true
}
if r >= 97 && r <= 122 {
return true
}
return false
}
func numberMatcher(r rune) bool {
if r >= 48 && r <= 57 {
return true
}
return false
}
func alphaNumericMatcher(r rune) bool {
return alphaMatcher(r) || numberMatcher(r)
}
func wordMatcher(r rune) bool {
if alphaNumericMatcher(r) {
return true
}
if r == '_' {
return true
}
return false
}
func (g *GUI) handleMouseRemotely(x, y int, pressedLeft, pressedMiddle, pressedRight, released, moved bool) bool {
tx, ty := 1+(x/g.fontManager.CharSize().X), 1+(y/g.fontManager.CharSize().Y)
mode := g.terminal.GetMouseMode()
switch mode {
case termutil.MouseModeNone:
return false
case termutil.MouseModeX10:
var button rune
switch true {
case pressedLeft:
button = 0
case pressedMiddle:
button = 1
case pressedRight:
button = 2
default:
return true
}
packet := fmt.Sprintf("\x1b[M%c%c%c", (rune(button + 32)), (rune(tx + 32)), (rune(ty + 32)))
_ = g.terminal.WriteToPty([]byte(packet))
return true
case termutil.MouseModeVT200, termutil.MouseModeButtonEvent:
var button rune
extMode := g.terminal.GetMouseExtMode()
switch true {
case pressedLeft:
button = 0
case pressedMiddle:
button = 1
case pressedRight:
button = 2
case released:
if extMode != termutil.MouseExtSGR {
button = 3
}
default:
return true
}
if moved && mode == termutil.MouseModeButtonEvent {
button |= 32
}
if ebiten.IsKeyPressed(ebiten.KeyShift) {
button |= 4
}
if ebiten.IsKeyPressed(ebiten.KeyMeta) {
button |= 8
}
if ebiten.IsKeyPressed(ebiten.KeyControl) {
button |= 16
}
var packet string
if extMode == termutil.MouseExtSGR {
final := 'M'
if released {
final = 'm'
}
packet = fmt.Sprintf("\x1b[<%d;%d;%d%c", button, tx, ty, final)
} else {
packet = fmt.Sprintf("\x1b[M%c%c%c", button+32, tx+32, ty+32)
}
g.terminal.WriteToPty([]byte(packet))
return true
}
return false
}
func (g *GUI) SetCursorToPointer() {
ebiten.SetCursorShape(ebiten.CursorShapePointer)
}
func (g *GUI) ResetCursor() {
ebiten.SetCursorShape(ebiten.CursorShapeDefault)
}