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

296 lines
7.6 KiB
Go

package main
import (
"image"
"gioui.org/font"
"gioui.org/layout"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/widget"
"gioui.org/widget/material"
"loov.dev/lensm/internal/codeview"
"loov.dev/lensm/internal/disasm"
"loov.dev/lensm/internal/gui"
)
type CodeTab struct {
Name string
Func disasm.Func
Code codeview.UI
Preview bool
Tab widget.Clickable
Close widget.Clickable
}
func (ui *FileUI) activeTab() *CodeTab {
if !gui.InRange(ui.ActiveTab, len(ui.CodeTabs)) {
return nil
}
return ui.CodeTabs[ui.ActiveTab]
}
func (ui *FileUI) activeCode() *codeview.UI {
tab := ui.activeTab()
if tab == nil {
return nil
}
return &tab.Code
}
func (ui *FileUI) loadTabCode(tab *CodeTab, fn disasm.Func) {
tab.Name = fn.Name()
tab.Func = fn
tab.Code = codeview.UI{}
tab.Code.Code, ui.LoadError = fn.Load(ui.loadOptions())
tab.Code.SelectedAsm = -1
tab.Code.SelectedView = codeview.ViewGoAsm
tab.Code.ResetScroll()
}
func (ui *FileUI) appendCodeTab(fn disasm.Func) *CodeTab {
if fn == nil {
return nil
}
tab := &CodeTab{}
ui.loadTabCode(tab, fn)
ui.CodeTabs = append(ui.CodeTabs, tab)
return tab
}
// previewIndex returns the index of the single preview tab, or -1.
func (ui *FileUI) previewIndex() int {
for i, tab := range ui.CodeTabs {
if tab.Preview {
return i
}
}
return -1
}
// previewTab shows fn in a single reusable preview tab. Browsing the function
// list (keyboard up/down or clicking a name) replaces the preview in place
// instead of piling up permanent tabs. Clicking the tab keeps it open, see
// selectTab, which clears Preview.
func (ui *FileUI) previewTab(fn disasm.Func) *CodeTab {
if fn == nil {
return nil
}
name := fn.Name()
tab := ui.findCodeTab(name, fn)
if tab == nil {
if slot := ui.previewIndex(); slot >= 0 {
tab = ui.CodeTabs[slot]
ui.loadTabCode(tab, fn)
ui.ActiveTab = slot
} else {
tab = ui.appendCodeTab(fn)
if tab == nil {
return nil
}
ui.ActiveTab = len(ui.CodeTabs) - 1
}
tab.Preview = true
}
ui.selectFuncByName(name)
ui.commentKey = ""
ui.recordNavigation(name)
ui.saveSessionState()
return tab
}
// openTab opens fn in a tab and activates it. When next is true a newly
// created tab is inserted right after the active tab instead of appended
// at the end; an already-open tab is activated in place regardless.
func (ui *FileUI) openTab(fn disasm.Func, next bool) *CodeTab {
if fn == nil {
return nil
}
name := fn.Name()
tab := ui.findCodeTab(name, fn)
if tab == nil {
tab = ui.appendCodeTab(fn)
if tab == nil {
return nil
}
index := len(ui.CodeTabs) - 1
if next && gui.InRange(ui.ActiveTab, index) {
at := ui.ActiveTab + 1
if at < index {
copy(ui.CodeTabs[at+1:], ui.CodeTabs[at:index])
ui.CodeTabs[at] = tab
index = at
}
}
ui.ActiveTab = index
}
tab.Preview = false
ui.selectFuncByName(name)
ui.commentKey = ""
ui.recordNavigation(name)
ui.saveSessionState()
return tab
}
// findCodeTab returns the open tab for name, refreshing its Func and
// making it active, or nil if no tab is open for it.
func (ui *FileUI) findCodeTab(name string, fn disasm.Func) *CodeTab {
for i, tab := range ui.CodeTabs {
if tab.Name == name {
tab.Func = fn
ui.ActiveTab = i
return tab
}
}
return nil
}
// keepActiveTab promotes the active preview tab to a permanent one, called
// when the user acts on the tab's content.
func (ui *FileUI) keepActiveTab() {
if tab := ui.activeTab(); tab != nil && tab.Preview {
tab.Preview = false
ui.saveSessionState()
}
}
func (ui *FileUI) selectTab(index int) {
if !gui.InRange(index, len(ui.CodeTabs)) {
return
}
ui.ActiveTab = index
ui.CodeTabs[index].Preview = false
ui.commentKey = ""
ui.selectFuncByName(ui.CodeTabs[index].Name)
ui.recordNavigation(ui.CodeTabs[index].Name)
ui.saveSessionState()
}
func (ui *FileUI) closeTab(index int) {
if !gui.InRange(index, len(ui.CodeTabs)) {
return
}
ui.CodeTabs = append(ui.CodeTabs[:index], ui.CodeTabs[index+1:]...)
switch {
case len(ui.CodeTabs) == 0:
ui.ActiveTab = -1
ui.commentKey = ""
ui.Funcs.Selected = ""
ui.Funcs.SelectedItem = nil
ui.Funcs.List.Selected = -1
case ui.ActiveTab > index:
ui.ActiveTab--
case ui.ActiveTab == index && ui.ActiveTab >= len(ui.CodeTabs):
ui.ActiveTab = len(ui.CodeTabs) - 1
}
if tab := ui.activeTab(); tab != nil {
ui.selectFuncByName(tab.Name)
ui.recordNavigation(tab.Name)
} else {
ui.commentKey = ""
}
ui.saveSessionState()
}
func (ui *FileUI) layoutCodeTabs(gtx layout.Context, colors gui.UIColors) layout.Dimensions {
for i := 0; i < len(ui.CodeTabs); i++ {
tab := ui.CodeTabs[i]
for tab.Tab.Clicked(gtx) {
ui.selectTab(i)
}
closed := false
for tab.Close.Clicked(gtx) {
ui.closeTab(i)
closed = true
}
if closed {
i--
}
}
if len(ui.CodeTabs) == 0 {
return layout.Dimensions{}
}
height := max(gtx.Metric.Dp(22), 20)
availableWidth := gtx.Constraints.Max.X
gtx.Constraints = layout.Exact(image.Pt(availableWidth, height))
paint.FillShape(gtx.Ops, colors.SecondaryBackground, clip.Rect{Max: gtx.Constraints.Max}.Op())
tabWidth := gtx.Metric.Dp(220)
minTabWidth := gtx.Metric.Dp(120)
if len(ui.CodeTabs) <= 3 && availableWidth > 0 {
tabWidth = min(tabWidth, max(minTabWidth, availableWidth/len(ui.CodeTabs)))
}
if tabWidth < minTabWidth {
tabWidth = minTabWidth
}
list := material.List(ui.Theme.Theme, &ui.Tabs)
list.AnchorStrategy = material.Overlay
return list.Layout(gtx, len(ui.CodeTabs), func(gtx layout.Context, index int) layout.Dimensions {
gtx.Constraints = layout.Exact(image.Pt(tabWidth, height))
return ui.layoutCodeTab(gtx, colors, ui.CodeTabs[index], index == ui.ActiveTab)
})
}
func (ui *FileUI) layoutCodeTab(gtx layout.Context, colors gui.UIColors, tab *CodeTab, active bool) layout.Dimensions {
size := gtx.Constraints.Max
defer clip.Rect{Max: size}.Push(gtx.Ops).Pop()
bg := colors.SecondaryBackground
if active {
bg = colors.Background
} else if tab.Tab.Hovered() {
bg = colors.Selection
}
paint.FillShape(gtx.Ops, bg, clip.Rect{Max: size}.Op())
if active {
paint.FillShape(gtx.Ops, ui.Theme.ContrastBg, clip.Rect{Max: image.Pt(size.X, 2)}.Op())
}
paint.FillShape(gtx.Ops, colors.Splitter, clip.Rect{
Min: image.Pt(size.X-1, 0),
Max: size,
}.Op())
layout.Inset{Top: 1, Right: 4, Bottom: 1, Left: 8}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
gtx.Constraints = layout.Exact(gtx.Constraints.Max)
return tab.Tab.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
size := gtx.Constraints.Max
defer clip.Rect{Max: size}.Push(gtx.Ops).Pop()
label := ui.Theme.Label(tab.Name, 0.8)
label.MaxLines = 1
if active {
label.Font.Weight = font.Black
}
if tab.Preview {
label.Font.Style = font.Italic
}
dims := layout.W.Layout(gtx, label.Layout)
return layout.Dimensions{Size: size, Baseline: dims.Baseline}
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
closeWidth := gtx.Metric.Dp(22)
gtx.Constraints = layout.Exact(image.Pt(closeWidth, gtx.Constraints.Max.Y))
return tab.Close.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
size := gtx.Constraints.Max
if tab.Close.Hovered() {
paint.FillShape(gtx.Ops, colors.Selection, clip.Rect{Max: size}.Op())
}
label := ui.Theme.Muted("x", 0.8)
label.MaxLines = 1
dims := layout.Center.Layout(gtx, label.Layout)
return layout.Dimensions{Size: size, Baseline: dims.Baseline}
})
}),
)
})
return layout.Dimensions{Size: size}
}