Cell canvas renderer with frame cache and a perceptual 256-colour quantizer; responsive geometry (large/medium/small/too-small); squarified treemap with nested previews, LOD grouping, category colouring and adaptive labels; spatial arrow navigation with stable NodeID selection; zoom and breadcrumbs; modal system (info, help, confirm, save); list, file-type, top-N, scan-info, duplicate and comparison views; live search/filter; mouse support; move-to-trash.
111 lines
2.2 KiB
Go
111 lines
2.2 KiB
Go
package tui
|
|
|
|
import (
|
|
"time"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"github.com/andydixon/mapsize/internal/inventory"
|
|
)
|
|
|
|
const doubleClick = 400 * time.Millisecond
|
|
|
|
func (m *Model) mouseClick(ev tea.Mouse) tea.Cmd {
|
|
t := m.tree
|
|
t.RLock()
|
|
defer t.RUnlock()
|
|
m.dirty = true
|
|
if len(m.modals) > 0 {
|
|
// Clicking outside the modal closes it.
|
|
if len(m.hits) > 0 && !m.hits[0].r.Contains(ev.X, ev.Y) {
|
|
m.closeModal()
|
|
}
|
|
return nil
|
|
}
|
|
if ev.Button == tea.MouseRight {
|
|
for _, h := range m.hits {
|
|
if h.r.Contains(ev.X, ev.Y) {
|
|
h.fn(m, false)
|
|
return m.inspectSelection()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if ev.Button != tea.MouseLeft {
|
|
return nil
|
|
}
|
|
for i := len(m.hits) - 1; i >= 0; i-- {
|
|
h := m.hits[i]
|
|
if h.r.Contains(ev.X, ev.Y) {
|
|
key := int64(ev.Y)<<32 | int64(i)
|
|
double := time.Since(m.lastClick) < doubleClick && m.lastClickI == key
|
|
m.lastClick, m.lastClickI = time.Now(), key
|
|
if double {
|
|
m.lastClickI = noSel
|
|
}
|
|
return h.fn(m, double)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// clickBlock selects a treemap block; a double click zooms (or inspects).
|
|
func (m *Model) clickBlock(id int64, double bool) tea.Cmd {
|
|
m.sel = id
|
|
if !double {
|
|
return nil
|
|
}
|
|
if !isGroup(id) && !m.tree.Node(inventory.NodeID(id)).IsDir() {
|
|
return m.inspectSelection()
|
|
}
|
|
return m.zoomIntoSelection()
|
|
}
|
|
|
|
func (m *Model) mouseWheel(ev tea.Mouse) tea.Cmd {
|
|
t := m.tree
|
|
t.RLock()
|
|
defer t.RUnlock()
|
|
m.dirty = true
|
|
up := ev.Button == tea.MouseWheelUp
|
|
if ev.Button != tea.MouseWheelUp && ev.Button != tea.MouseWheelDown {
|
|
return nil
|
|
}
|
|
if len(m.modals) > 0 {
|
|
f := m.modals[len(m.modals)-1].(*modalFrame)
|
|
if up {
|
|
f.scroll -= 3
|
|
} else {
|
|
f.scroll += 3
|
|
}
|
|
f.clampScroll()
|
|
return nil
|
|
}
|
|
if up {
|
|
// Zoom towards the block under the pointer.
|
|
if l := m.tm; l != nil && m.view == 0 {
|
|
for _, b := range l.blocks {
|
|
if b.Contains(ev.X, ev.Y) {
|
|
m.sel = b.ID
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return m.views[m.view].wheel(m, up)
|
|
}
|
|
|
|
func (m *Model) mouseMotion(ev tea.Mouse) {
|
|
h := noSel
|
|
if l := m.tm; l != nil && m.view == 0 && len(m.modals) == 0 {
|
|
for _, b := range l.blocks {
|
|
if b.Contains(ev.X, ev.Y) {
|
|
h = b.ID
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if h != m.hover {
|
|
m.hover = h
|
|
m.dirty = true
|
|
}
|
|
}
|