Files
wehub-resource-sync c4536f7e05
CI / test (push) Failing after 1s
CI / macOS amd64 (push) Has been cancelled
CI / macOS arm64 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:31 +08:00

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
}