359 lines
8.1 KiB
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)
|
|
}
|