1package ui23import (4 "strings"56 tea "charm.land/bubbletea/v2"7 "charm.land/lipgloss/v2"89 "src.rest/cat/012/internal/ui/overlay"10)1112// openOverlay shows o, replacing any open overlay.13func (m *Model) openOverlay(o overlay.Overlay) {14 m.overlay = o15 m.mode = modeMenu16}1718// closeOverlay closes the open overlay and returns to READY.19func (m *Model) closeOverlay() {20 m.overlay = nil21 m.line.Clear()22 if m.mode == modeMenu {23 m.mode = modeReady24 }25}2627// runFromOverlay closes the overlay, then runs a command, which may open28// a prompt or another overlay of its own. Unavailable commands do nothing.29func (m *Model) runFromOverlay(id string) tea.Cmd {30 c, ok := commands[id]31 if !ok || !c.available(m) {32 return nil33 }34 m.closeOverlay()35 return m.runCommand(id)36}3738// compositor stacks boxes over the screen in order, each on its own39// layer so hit tests find the topmost box.40func compositor(boxes []overlay.Box) *lipgloss.Compositor {41 layers := make([]*lipgloss.Layer, len(boxes))42 for i, b := range boxes {43 layers[i] = lipgloss.NewLayer(strings.Join(b.Lines, "\n")).X(b.X).Y(b.Y).Z(i + 1).ID(b.ID)44 }45 return lipgloss.NewCompositor(layers...)46}4748// floating returns the boxes drawn over the screen: the open overlay's,49// or the formula suggestions while typing.50func (m *Model) floating() []overlay.Box {51 if m.overlay != nil {52 return m.overlay.Layout()53 }54 if b, ok := m.entry.assist.Box(m.host()); ok {55 return []overlay.Box{b}56 }57 if b, ok := m.noteBox(); ok {58 return []overlay.Box{b}59 }60 return m.notebookBoxes()61}6263// compose draws the charts floating over the grid, then boxes (the open64// overlay or formula suggestions), over the rendered screen.65func (m *Model) compose(screen string, boxes []overlay.Box) string {66 c := lipgloss.NewCanvas(m.width, m.height)67 c.Compose(lipgloss.NewLayer(screen))68 if charts := m.chartBoxes(); len(charts) > 0 {69 c.Compose(compositor(charts))70 }71 if len(boxes) > 0 {72 c.Compose(compositor(boxes))73 }74 return c.Render()75}7677// shellMouse gives the open overlay, or else the menu bar, the first look78// at a mouse message. It reports false for messages the grid handles.79func (m *Model) shellMouse(msg tea.MouseMsg) (tea.Cmd, bool) {80 if cmd, ok := m.sourceMouse(msg); ok {81 return cmd, true82 }83 if cmd, ok := m.notebookMouse(msg); ok {84 return cmd, true85 }86 if m.overlay != nil {87 return m.overlayMouse(msg), true88 }89 if m.entry.assist.Mouse(m.host(), msg) {90 return nil, true91 }92 click, ok := msg.(tea.MouseClickMsg)93 if !ok || m.mode != modeReady {94 return nil, false95 }96 mouse := click.Mouse()97 if cmd, ok := m.chartClick(mouse); ok {98 return cmd, true99 }100 switch {101 case mouse.Button == tea.MouseRight:102 m.rightClick(mouse.X, mouse.Y)103 return nil, true104 case mouse.Y == menuLine && mouse.Button == tea.MouseLeft:105 if i := barMenuAt(mouse.X); i >= 0 {106 m.showBarMenu(i)107 return nil, true108 }109 }110 return nil, false111}112113// overlayMouse hands a mouse message to the open overlay, with the box and114// position under the mouse.115func (m *Model) overlayMouse(msg tea.MouseMsg) tea.Cmd {116 mouse := msg.Mouse()117 e := overlay.MouseEvent{Button: mouse.Button, X: mouse.X, Y: mouse.Y}118 switch msg.(type) {119 case tea.MouseClickMsg:120 e.Kind = overlay.MousePress121 case tea.MouseMotionMsg:122 e.Kind = overlay.MouseMotion123 case tea.MouseReleaseMsg:124 e.Kind = overlay.MouseRelease125 case tea.MouseWheelMsg:126 e.Kind = overlay.MouseWheel127 }128 if h := compositor(m.overlay.Layout()).Hit(e.X, e.Y); !h.Empty() {129 e.Box = h.ID()130 e.Col, e.Row = e.X-h.Bounds().Min.X, e.Y-h.Bounds().Min.Y131 }132 return m.overlay.Mouse(e)133}