package tui import tea "github.com/charmbracelet/bubbletea" // Layout offsets for hit-testing the rendered views (see view.go). Every // screen renders a title line then a blank line before its content, so content // starts at headerRows. The grid is drawn with a border between cells, hence // the +1s. const ( headerRows = 2 // title line + blank line cellStride = cellW + 1 // cell inner width + right border column blockH = cellH + 1 // cell content rows + top border row ) // handleMouse routes mouse events per screen. Enables click-to-select on the // list screens and tmux-style click/drag editing in the layout grid. func (m *model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) { switch m.screen { case screenMenu, screenCameras, screenLayouts, screenSetActive: return m.mouseList(msg) case screenLayoutEdit: return m.mouseGridEdit(msg) case screenCameraPicker: return m.mousePicker(msg) } return m, nil } // cellAtMouse maps a screen position to a grid cell in the editor/picker. func (m *model) cellAtMouse(x, y int) (col, row int, ok bool) { cols, rows := m.gridDims() if y < headerRows || x < 0 { return 0, 0, false } row = (y - headerRows) / blockH col = x / cellStride if row < 0 || row >= rows || col < 0 || col >= cols { return 0, 0, false } return col, row, true } // gridLineCount is how many terminal rows renderGrid emits. func (m *model) gridLineCount() int { _, rows := m.gridDims() return rows*blockH + 1 } // listLen is the number of selectable rows on the current list screen. func (m *model) listLen() int { switch m.screen { case screenMenu: return len(menuItems) case screenCameras: return len(m.cfg.Cameras) case screenLayouts, screenSetActive: return len(m.cfg.Layouts) } return 0 } // mouseList: left-click a row to select and activate it (same as pressing enter). func (m *model) mouseList(msg tea.MouseMsg) (tea.Model, tea.Cmd) { if msg.Action != tea.MouseActionRelease || msg.Button != tea.MouseButtonLeft { return m, nil } idx := msg.Y - headerRows if idx < 0 || idx >= m.listLen() { return m, nil } m.cursor = idx return m.handleKey(tea.KeyMsg{Type: tea.KeyEnter}) } // mouseGridEdit implements tmux-style editing: click a cell to assign a camera, // or press on a tile and drag to resize its span (grid-snapped). func (m *model) mouseGridEdit(msg tea.MouseMsg) (tea.Model, tea.Cmd) { if msg.Button != tea.MouseButtonLeft { return m, nil } col, row, ok := m.cellAtMouse(msg.X, msg.Y) switch msg.Action { case tea.MouseActionPress: if !ok { return m, nil } m.edCol, m.edRow = col, row m.dragTile = m.tileIndexAt(col, row) m.dragging = m.dragTile >= 0 m.dragMoved = false case tea.MouseActionMotion: if !ok || !m.dragging { return m, nil } t := &m.curLayout().Tiles[m.dragTile] newCS, newRS := col-t.Col+1, row-t.Row+1 // Only grow/shrink toward the bottom-right of the tile's origin. if newCS >= 1 && newRS >= 1 && m.regionFree(m.dragTile, t.Col, t.Row, newCS, newRS) { t.ColSpan, t.RowSpan = newCS, newRS m.edCol, m.edRow = col, row m.dragMoved = true m.dirty = true } case tea.MouseActionRelease: wasDrag := m.dragging && m.dragMoved m.dragging = false m.dragMoved = false // A plain click (no drag) opens the camera picker for that cell. if ok && !wasDrag { m.edCol, m.edRow = col, row m.screen, m.cursor = screenCameraPicker, 0 } } return m, nil } // mousePicker: left-click an option to choose it. func (m *model) mousePicker(msg tea.MouseMsg) (tea.Model, tea.Cmd) { if msg.Action != tea.MouseActionRelease || msg.Button != tea.MouseButtonLeft { return m, nil } // Options are drawn below the grid and a blank separator line. optStart := headerRows + m.gridLineCount() + 1 idx := msg.Y - optStart if idx < 0 || idx >= len(m.pickerOptions()) { return m, nil } m.cursor = idx return m.handleKey(tea.KeyMsg{Type: tea.KeyEnter}) }