fix(tui): align floating menu hit testing

This commit is contained in:
Vincent Koc 2026-05-03 03:47:06 -07:00
parent a4303eebc4
commit aff4efbf5d
No known key found for this signature in database
2 changed files with 84 additions and 14 deletions

View File

@ -674,13 +674,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.MouseMsg:
if typed.Action == tea.MouseActionMotion && typed.Button == tea.MouseButtonNone {
if m.menuOpen {
m.handleMenuMouse(typed)
return m, m.handleMenuMouse(typed)
}
return m, nil
}
if m.menuOpen {
m.handleMenuMouse(typed)
return m, nil
return m, m.handleMenuMouse(typed)
}
switch {
case typed.Type == tea.MouseWheelUp || typed.Button == tea.MouseButtonWheelUp:
@ -937,47 +936,47 @@ func (m *model) clearLastClick() {
m.lastClickAt = time.Time{}
}
func (m *model) handleMenuMouse(msg tea.MouseMsg) {
func (m *model) handleMenuMouse(msg tea.MouseMsg) tea.Cmd {
switch {
case msg.Type == tea.MouseWheelUp || msg.Button == tea.MouseButtonWheelUp:
m.menuIndex = m.nextSelectableMenuIndex(-1)
m.keepMenuVisible()
return
return nil
case msg.Type == tea.MouseWheelDown || msg.Button == tea.MouseButtonWheelDown:
m.menuIndex = m.nextSelectableMenuIndex(1)
m.keepMenuVisible()
return
return nil
case msg.Button == tea.MouseButtonRight && msg.Action == tea.MouseActionPress:
m.closeMenu()
return
return nil
}
index, ok := m.menuIndexAtMouse(msg.X, msg.Y)
if msg.Action == tea.MouseActionMotion {
if !ok || index < 0 || index >= len(m.menuItems) {
return
return nil
}
m.menuIndex = m.nearestSelectableMenuIndex(index, 1)
m.keepMenuVisible()
return
return nil
}
if msg.Button != tea.MouseButtonLeft || msg.Action != tea.MouseActionPress {
return
return nil
}
if !ok {
m.closeMenu()
return
return nil
}
if index < 0 || index >= len(m.menuItems) {
return
return nil
}
if !m.menuItems[index].selectable() {
m.menuIndex = m.nearestSelectableMenuIndex(index, 1)
m.keepMenuVisible()
return
return nil
}
m.menuIndex = index
m.keepMenuVisible()
_ = m.runMenuItem(m.menuItems[m.menuIndex])
return m.runMenuItem(m.menuItems[m.menuIndex])
}
func (m model) menuIndexAtMouse(x, y int) (int, bool) {
@ -985,6 +984,7 @@ func (m model) menuIndexAtMouse(x, y int) (int, bool) {
rowOffset := 4
if m.menuFloating {
menuRect = m.menuRect
rowOffset = 3
}
if !menuRect.contains(x, y) {
return 0, false

View File

@ -1317,6 +1317,76 @@ func TestRightClickPlacesFloatingMenu(t *testing.T) {
}
}
func TestMouseClickUsesFloatingMenuOffset(t *testing.T) {
m := newModel(Options{Title: "archive", Items: []Item{{Title: "alpha"}}})
m.width = 140
m.height = 32
m.menuOpen = true
m.menuFloating = true
m.menuRect = rect{x: 5, y: 3, w: 40, h: 12}
m.menuOff = 5
m.menuItems = make([]menuItem, 8)
for index := range m.menuItems {
m.menuItems[index] = menuItem{label: fmt.Sprintf("Item %d", index), action: actionQuit}
}
updated, cmd := m.Update(tea.MouseMsg{
X: m.menuRect.x + 2,
Y: m.menuRect.y + 3,
Button: tea.MouseButtonLeft,
Action: tea.MouseActionPress,
})
m = updated.(model)
if m.menuIndex != 5 {
t.Fatalf("floating menu click selected %d, want offset row 5", m.menuIndex)
}
if cmd == nil {
t.Fatal("floating menu click did not run selected item")
}
if !m.menuOpen || !m.menuFloating {
t.Fatalf("floating menu should stay open for submenu-like actions, open=%v floating=%v", m.menuOpen, m.menuFloating)
}
}
func TestMouseMotionHoversFloatingMenuItems(t *testing.T) {
m := newModel(Options{Title: "archive", Items: []Item{{Title: "alpha"}}})
m.width = 140
m.height = 32
m.menuOpen = true
m.menuFloating = true
m.menuRect = rect{x: 5, y: 3, w: 40, h: 12}
m.menuOff = 1
m.menuItems = make([]menuItem, 6)
for index := range m.menuItems {
m.menuItems[index] = menuItem{label: fmt.Sprintf("Item %d", index), action: actionClose}
}
updated, _ := m.Update(tea.MouseMsg{
X: m.menuRect.x + 2,
Y: m.menuRect.y + 5,
Button: tea.MouseButtonNone,
Action: tea.MouseActionMotion,
})
m = updated.(model)
if m.menuIndex != 3 {
t.Fatalf("hover selected %d, want item 3", m.menuIndex)
}
updated, _ = m.Update(tea.MouseMsg{
X: m.menuRect.x + 2,
Y: m.menuRect.y + 6,
Button: tea.MouseButtonRight,
Action: tea.MouseActionMotion,
})
m = updated.(model)
if m.menuIndex != 4 {
t.Fatalf("right-button hover selected %d, want item 4", m.menuIndex)
}
}
func TestClickingRowsHeaderSorts(t *testing.T) {
m := newModel(Options{
Title: "archive",