297 lines
9.2 KiB
Go
297 lines
9.2 KiB
Go
package codeview
|
|
|
|
import (
|
|
"image"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
|
|
"gioui.org/f32"
|
|
"gioui.org/font"
|
|
"gioui.org/io/event"
|
|
"gioui.org/io/key"
|
|
"gioui.org/io/pointer"
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"gioui.org/widget"
|
|
|
|
"loov.dev/lensm/internal/disasm"
|
|
"loov.dev/lensm/internal/gui"
|
|
)
|
|
|
|
// handleInput processes pointer and keyboard events for the frame,
|
|
// updating selection and scroll state, and reports whether the release
|
|
// was a click (as opposed to a drag).
|
|
func (ui Style) handleInput(gtx layout.Context, c codeColumns) (mouseClicked bool) {
|
|
lineHeight := c.lineHeight
|
|
event.Op(gtx.Ops, ui.UI)
|
|
selectionAt := func(position f32.Point) (View, int, bool) {
|
|
if c.asm.Contains(position.X) {
|
|
relative := position.Y - ui.asm.Offset
|
|
if relative < 0 {
|
|
return ViewNone, -1, false
|
|
}
|
|
line := int(relative) / lineHeight
|
|
return ViewGoAsm, line, gui.InRange(line, len(ui.Code.Insts))
|
|
}
|
|
if ui.ShowNative && c.native.Contains(position.X) {
|
|
relative := position.Y - ui.asm.Offset
|
|
if relative < 0 {
|
|
return ViewNone, -1, false
|
|
}
|
|
line := int(relative) / lineHeight
|
|
return ViewNativeAsm, line, gui.InRange(line, len(ui.Code.Insts))
|
|
}
|
|
if c.source.Contains(position.X) {
|
|
line := sourceRowAtY(ui.Code, ui.src.Offset, lineHeight, position.Y)
|
|
return ViewSource, line, line >= 0
|
|
}
|
|
return ViewNone, -1, false
|
|
}
|
|
// selectionDragLine clamps a drag position to the selection view's
|
|
// content, so a fast drag past an edge selects through to the first
|
|
// or last line instead of stopping at the last in-range sample.
|
|
selectionDragLine := func(view View, position f32.Point) (int, bool) {
|
|
var line, count int
|
|
switch view {
|
|
case ViewGoAsm, ViewNativeAsm:
|
|
count = len(ui.Code.Insts)
|
|
line = int(position.Y-ui.asm.Offset) / lineHeight
|
|
case ViewSource:
|
|
count = sourceRowCount(ui.Code)
|
|
line = int(position.Y-ui.src.Offset) / lineHeight
|
|
default:
|
|
return -1, false
|
|
}
|
|
if count == 0 {
|
|
return -1, false
|
|
}
|
|
return min(max(line, 0), count-1), true
|
|
}
|
|
for {
|
|
ev, ok := gtx.Event(pointer.Filter{
|
|
Target: ui.UI,
|
|
Kinds: pointer.Move | pointer.Leave | pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel | pointer.Scroll,
|
|
ScrollY: pointer.ScrollRange{
|
|
Min: int(ui.asm.Offset) - lineHeight,
|
|
Max: len(ui.Code.Insts)*lineHeight + lineHeight - int(ui.asm.Offset),
|
|
},
|
|
})
|
|
if !ok {
|
|
break
|
|
}
|
|
if ev, ok := ev.(pointer.Event); ok {
|
|
switch ev.Kind {
|
|
case pointer.Move:
|
|
ui.mousePosition = ev.Position
|
|
case pointer.Leave:
|
|
if !ui.selecting {
|
|
ui.mousePosition = f32.Pt(-1, -1)
|
|
}
|
|
case pointer.Press:
|
|
ui.mousePosition = ev.Position
|
|
if ev.Buttons.Contain(pointer.ButtonPrimary) {
|
|
if ui.OnInteract != nil {
|
|
ui.OnInteract()
|
|
}
|
|
view, line, selectable := selectionAt(ev.Position)
|
|
if selectable {
|
|
ui.Selection.Begin(view, line, ev.Modifiers.Contain(key.ModShift))
|
|
ui.selecting = true
|
|
ui.selectionPointer = ev.PointerID
|
|
ui.selectionStart = ev.Position
|
|
ui.selectionMoved = false
|
|
gtx.Execute(pointer.GrabCmd{Tag: ui.UI, ID: ev.PointerID})
|
|
gtx.Execute(key.FocusCmd{Tag: ui.UI})
|
|
} else {
|
|
ui.Selection.Clear()
|
|
}
|
|
}
|
|
case pointer.Drag:
|
|
ui.mousePosition = ev.Position
|
|
if ui.selecting && ev.PointerID == ui.selectionPointer {
|
|
if math.Abs(float64(ev.Position.X-ui.selectionStart.X)) > 3 || math.Abs(float64(ev.Position.Y-ui.selectionStart.Y)) > 3 {
|
|
ui.selectionMoved = true
|
|
}
|
|
if line, ok := selectionDragLine(ui.Selection.View, ev.Position); ok {
|
|
ui.Selection.Extend(ui.Selection.View, line)
|
|
}
|
|
}
|
|
case pointer.Release:
|
|
ui.mousePosition = ev.Position
|
|
if ui.selecting && ev.PointerID == ui.selectionPointer {
|
|
if line, ok := selectionDragLine(ui.Selection.View, ev.Position); ok {
|
|
ui.Selection.Extend(ui.Selection.View, line)
|
|
}
|
|
mouseClicked = !ui.selectionMoved
|
|
ui.selecting = false
|
|
}
|
|
case pointer.Cancel:
|
|
ui.selecting = false
|
|
case pointer.Scroll:
|
|
ui.mousePosition = ev.Position
|
|
switch {
|
|
case c.asm.Contains(ev.Position.X):
|
|
ui.asm.Offset -= ev.Scroll.Y
|
|
case ui.ShowNative && c.native.Contains(ev.Position.X):
|
|
ui.asm.Offset -= ev.Scroll.Y
|
|
case c.source.Contains(ev.Position.X):
|
|
ui.src.Offset -= ev.Scroll.Y
|
|
}
|
|
}
|
|
}
|
|
}
|
|
selectionFocus := event.Tag(ui.UI)
|
|
commentFocused := ui.CommentEditor != nil && gtx.Focused(ui.CommentEditor)
|
|
if ui.Selection.Active && !commentFocused {
|
|
// A drag can leave keyboard focus on a surrounding widget on macOS.
|
|
// An active line selection still owns Cmd/Ctrl+C, Cmd/Ctrl+A, and
|
|
// Escape unless the user is editing comment text. (A focused text
|
|
// editor elsewhere still wins: it polls earlier in layout order.)
|
|
selectionFocus = nil
|
|
}
|
|
for {
|
|
ev, ok := gtx.Event(key.Filter{Focus: selectionFocus, Required: key.ModShortcut, Name: key.Name("C")})
|
|
if !ok {
|
|
break
|
|
}
|
|
keyEvent, ok := ev.(key.Event)
|
|
if ok && keyEvent.State == key.Press {
|
|
if text := ui.Selection.Text(ui.Code); text != "" && ui.CopyText != nil {
|
|
ui.CopyText(gtx, text)
|
|
}
|
|
}
|
|
}
|
|
for {
|
|
ev, ok := gtx.Event(
|
|
key.FocusFilter{Target: ui.UI},
|
|
key.Filter{Focus: selectionFocus, Required: key.ModShortcut, Name: key.Name("A")},
|
|
key.Filter{Focus: selectionFocus, Name: key.NameEscape},
|
|
)
|
|
if !ok {
|
|
break
|
|
}
|
|
keyEvent, ok := ev.(key.Event)
|
|
if !ok || keyEvent.State != key.Press {
|
|
continue
|
|
}
|
|
switch keyEvent.Name {
|
|
case key.Name("A"):
|
|
view := ui.Selection.View
|
|
if view == ViewNone {
|
|
view = ViewGoAsm
|
|
}
|
|
lineCount := len(ui.Code.Insts)
|
|
if view == ViewSource {
|
|
lineCount = sourceRowCount(ui.Code)
|
|
}
|
|
if lineCount > 0 {
|
|
ui.Selection = TextSelection{View: view, Anchor: 0, Head: lineCount - 1, Active: true}
|
|
}
|
|
case key.NameEscape:
|
|
ui.Selection.Clear()
|
|
}
|
|
}
|
|
return mouseClicked
|
|
}
|
|
|
|
// resolveHover computes the pointer hover state and handles clicks on the
|
|
// assembly column: following call targets, selecting a line for comment
|
|
// editing, and activating jump animations.
|
|
func (ui Style) resolveHover(gtx layout.Context, c codeColumns, mouseClicked bool) codeHover {
|
|
lineHeight := c.lineHeight
|
|
mousePosition := ui.mousePosition
|
|
mouseInAsm := c.asm.Contains(mousePosition.X) || (ui.ShowNative && c.native.Contains(mousePosition.X))
|
|
mouseInSource := c.source.Contains(mousePosition.X)
|
|
if mouseInAsm || mouseInSource {
|
|
pointer.CursorText.Add(gtx.Ops)
|
|
}
|
|
highlightAsmIndex := -1
|
|
if relative := mousePosition.Y - ui.asm.Offset; mouseInAsm && relative >= 0 {
|
|
highlightAsmIndex = int(relative) / lineHeight
|
|
}
|
|
|
|
if gui.InRange(highlightAsmIndex, len(ui.Code.Insts)) {
|
|
activateClicked := mouseClicked && ui.SelectedAsm == highlightAsmIndex
|
|
ix := &ui.Code.Insts[highlightAsmIndex]
|
|
callTargetHovered := ui.TryOpen != nil &&
|
|
ix.Call != "" &&
|
|
c.asm.Contains(mousePosition.X) &&
|
|
mousePosition.X <= float32(c.goTextLeft+c.goInstructionWidth) &&
|
|
ui.callTargetHit(gtx, *ix, c.goTextLeft, mousePosition.X)
|
|
if callTargetHovered {
|
|
pointer.CursorPointer.Add(gtx.Ops)
|
|
if mouseClicked {
|
|
ui.SelectedAsm = highlightAsmIndex
|
|
ui.SelectedView = ViewGoAsm
|
|
ui.SelectedFile = ""
|
|
ui.SelectedLine = 0
|
|
ui.TryOpen(gtx, ix.Call)
|
|
}
|
|
} else if mouseClicked && ix.Text != "" {
|
|
// Spacer rows (empty synthetic instructions) have no inline
|
|
// editor; focusing it would swallow subsequent typing.
|
|
ui.SelectedAsm = highlightAsmIndex
|
|
ui.SelectedFile = ""
|
|
ui.SelectedLine = 0
|
|
if ui.ShowNative && c.native.Contains(mousePosition.X) {
|
|
ui.SelectedView = ViewNativeAsm
|
|
} else {
|
|
ui.SelectedView = ViewGoAsm
|
|
}
|
|
if ui.CommentEditor != nil {
|
|
gtx.Execute(key.FocusCmd{Tag: ui.CommentEditor})
|
|
}
|
|
}
|
|
if ix.Call == "" && ix.RefOffset != 0 {
|
|
pointer.CursorPointer.Add(gtx.Ops)
|
|
if activateClicked {
|
|
// TODO: smooth scroll
|
|
// highlightAsmIndex -= ix.RefOffset
|
|
ui.asm.Anim.Start(gtx, ui.asm.Offset, ui.asm.Offset-float32(ix.RefOffset*lineHeight), 150*time.Millisecond)
|
|
}
|
|
}
|
|
}
|
|
if !gui.InRange(ui.SelectedAsm, len(ui.Code.Insts)) {
|
|
ui.SelectedAsm = -1
|
|
}
|
|
|
|
return codeHover{
|
|
position: mousePosition,
|
|
inAsm: mouseInAsm,
|
|
inSource: mouseInSource,
|
|
asmIndex: highlightAsmIndex,
|
|
}
|
|
}
|
|
|
|
func (ui Style) callTargetHit(gtx layout.Context, inst disasm.Inst, left int, x float32) bool {
|
|
if inst.Call == "" {
|
|
return false
|
|
}
|
|
start := strings.Index(inst.Text, inst.Call)
|
|
if start < 0 {
|
|
return false
|
|
}
|
|
// A hovered call line is drawn bold and italic; measure with the
|
|
// same style, otherwise the hitbox drifts from the visible text
|
|
// whenever the fallback font is proportional.
|
|
f := font.Font{Typeface: "override-monospace,Go,monospace", Weight: font.Black, Style: font.Italic}
|
|
end := start + len(inst.Call)
|
|
targetLeft := left + ui.measureAsmTextWidth(gtx, f, inst.Text[:start])
|
|
targetRight := left + ui.measureAsmTextWidth(gtx, f, inst.Text[:end])
|
|
return float32(targetLeft) <= x && x <= float32(targetRight)
|
|
}
|
|
|
|
func (ui Style) measureAsmTextWidth(gtx layout.Context, f font.Font, text string) int {
|
|
if text == "" {
|
|
return 0
|
|
}
|
|
gtx.Constraints.Min = image.Point{}
|
|
gtx.Constraints.Max = image.Pt(gui.MaxLineWidth, gui.MaxLineWidth)
|
|
|
|
macro := op.Record(gtx.Ops)
|
|
dims := widget.Label{MaxLines: 1}.Layout(gtx, ui.Theme.Shaper, f, ui.TextHeight, text, op.CallOp{})
|
|
_ = macro.Stop()
|
|
return dims.Size.X
|
|
}
|