1package ui23import (4 tea "charm.land/bubbletea/v2"56 "src.rest/cat/012/internal/sheet"7 "src.rest/cat/012/internal/ui/nbview"8 "src.rest/cat/012/internal/ui/overlay"9)1011// Working in an output's grid (nbgrid.go). Enter on a table or record12// output, or a click in it, gives its grid the keys: the window shows13// the grid's active cell and selection, and keys, the mouse and the14// commands that act on a sheet's cells go to the grid's own model, so15// arrows, Shift+arrows, Ctrl+C, sorting and filtering from the menus,16// Ctrl+F and resizing columns work as on a sheet. What belongs to the17// notebook and the program (menus, the palette, help, saving, other18// tabs) stays theirs. Enter shows the grid full-screen, and Esc goes19// back a level: to the window, then to the notebook.2021// gridIn is the grid entered, while the notebook shown still shows it.22func (m *Model) gridIn() *outGrid {23 g := m.nb.out.in24 if g == nil {25 return nil26 }27 if v := m.nbView(); v == nil || !v.Shows(g) {28 m.nb.out.in = nil29 return nil30 }31 return g32}3334// enterGrid gives grid g the keys, reporting whether it could.35func (m *Model) enterGrid(g *outGrid) bool {36 if !g.ready(m.nbGridWidth(), nbview.Window) {37 m.note = "The output isn't a table: " + g.err38 return false39 }40 m.nb.out.in = g41 g.child.note, g.child.warn = "", ""42 return true43}4445// gridBack goes back a level from grid g: from full-screen to the46// window, and from the window to the notebook.47func (m *Model) gridBack() {48 if v := m.nbView(); v != nil && v.FullOpen() {49 v.CloseFull()50 v.FollowOutput()51 return52 }53 m.nb.out.in = nil54}5556// gridFull shows the grid entered full-screen.57func (m *Model) gridFull() {58 if v := m.nbView(); v != nil && !v.FullOpen() {59 v.OpenFull()60 }61}6263// gridKey gives a key to grid g, after the keys the notebook and the64// program keep while the grid is at rest.65func (m *Model) gridKey(g *outGrid, k tea.KeyPressMsg) tea.Cmd {66 c := g.child67 if c.mode == modeReady && c.overlay == nil && c.mouse.drag == dragNone {68 if cmd, ok := m.gridOwnKey(g, k.String()); ok {69 return cmd70 }71 }72 return g.forward(k)73}7475// gridOwnKey takes the keys the grid leaves to the notebook: Esc when76// nothing is selected, the menus, and the program's own commands.77func (m *Model) gridOwnKey(g *outGrid, key string) (tea.Cmd, bool) {78 if key == "esc" && !g.child.hasRange() {79 m.gridBack()80 return nil, true81 }82 if i := barMenuFor(key); i >= 0 {83 m.showBarMenu(i)84 return nil, true85 }86 if id, ok := keymap[canonicalKey(key)]; ok && notebookSafe(id) && !gridTakes(id) {87 return m.runCommand(id), true88 }89 return m.runShortcut(key)90}9192// gridTakes reports whether a command the notebook would run acts on93// the grid entered instead: undo and redo, of its sorts and filters.94func gridTakes(id string) bool { return id == "edit.undo" || id == "edit.redo" }9596// forward hands the grid a message, then keeps its active cell on the97// table, passes what it copied to the program's clipboard, and keeps98// its window on screen.99func (g *outGrid) forward(msg tea.Msg) tea.Cmd {100 c := g.child101 clip, focus := c.copied.clip, *c.focus()102 var cmd tea.Cmd103 g.roomy(func() { _, cmd = c.Update(msg) })104 if *c.focus() != focus {105 c.scrollTo(*c.focus()) // in the window, which is smaller106 }107 g.keep()108 if c.copied.clip != clip && c.copied.clip != nil {109 g.parent.copied = clipboard{clip: c.copied.clip, sheet: c.sheet}110 if c.note == "" {111 c.note = "Copied " + countCells(c.copied.clip.Range) + ": Ctrl+V pastes them on a sheet"112 }113 }114 if v := g.parent.nbView(); v != nil {115 v.FollowOutput()116 }117 return cmd118}119120// run runs a command on the grid.121func (g *outGrid) run(id string) tea.Cmd {122 cmd := g.child.runCommand(id)123 g.keep()124 return cmd125}126127// keep keeps the active cell and the selection's moving corner on the128// table's rows and columns, under its header.129func (g *outGrid) keep() {130 c := g.child131 in := func(a sheet.Addr) sheet.Addr {132 return sheet.Addr{Col: clamp(a.Col, 0, max(g.cols-1, 0)), Row: clamp(a.Row, 1, max(g.rows, 1))}133 }134 if c.whole != wholeNone {135 return136 }137 cur, ext := in(c.cur), in(c.ext)138 if cur != c.cur || ext != c.ext {139 c.cur, c.ext = cur, ext140 c.scrollTo(*c.focus())141 }142}143144// gridMouse takes a mouse message over an output's grid, or any while145// the grid entered drags or has a menu open, reporting whether it did.146func (m *Model) gridMouse(v *nbview.View, msg tea.MouseMsg) (tea.Cmd, bool) {147 mouse := msg.Mouse()148 in := m.gridIn()149 if in != nil && (in.child.overlay != nil || in.child.mouse.drag != dragNone || in.child.mode != modeReady) {150 return in.forward(in.toChild(v, msg)), true151 }152 found, _, _, ok := v.GridAt(mouse.X, mouse.Y-nbBody)153 g, _ := found.(*outGrid)154 if in != nil && g != in {155 in.child.mouse.hover = hit{}156 }157 if !ok || g == nil {158 return nil, false159 }160 switch msg := msg.(type) {161 case tea.MouseWheelMsg:162 if g != in || !sideways(tea.Mouse(msg)) {163 return nil, false // the notebook scrolls the window164 }165 case tea.MouseMotionMsg:166 if g != in {167 return nil, true168 }169 case tea.MouseClickMsg:170 if g != in {171 v.SelectGrid(g)172 if !m.enterGrid(g) {173 return nil, true174 }175 }176 }177 return g.forward(g.toChild(v, msg)), true178}179180// sideways reports whether a wheel turn scrolls sideways.181func sideways(mouse tea.Mouse) bool {182 return mouse.Button == tea.MouseWheelLeft || mouse.Button == tea.MouseWheelRight || mouse.Mod.Contains(tea.ModShift)183}184185// roomy runs fn with the child as large as the program's screen leaves186// it from the window down, so the boxes it opens (a column's filter, a187// menu) are laid out with the room the screen has rather than the188// window's.189func (g *outGrid) roomy(fn func()) {190 c, v := g.child, g.parent.nbView()191 if v == nil {192 fn()193 return194 }195 ox, oy := g.origin(v)196 w, h := c.width, c.height197 c.width, c.height = max(g.parent.width-ox, w), max(g.parent.height-oy+gridTop, h)198 defer func() { c.width, c.height = w, h }()199 fn()200}201202// origin is where the grid's column header is on the program's screen.203func (g *outGrid) origin(v *nbview.View) (x, y int) {204 x, y, _ = v.GridOrigin(g)205 return x, y + nbBody206}207208// The child's screen has its column header on headerLine, then the209// frozen header row and the divider under it, which the window leaves210// out, then its rows: childY and parentY map lines between the two.211212// childY is the child's line for line y of the window, 0 its header.213func childY(y int) int {214 if y <= 0 {215 return headerLine + y216 }217 return gridTop + 1 + y218}219220// windowY is the window's line for the child's line y.221func windowY(y int) int {222 if y <= headerLine {223 return y - headerLine224 }225 return max(y-gridTop-1, 1)226}227228// toChild is a mouse message with its position on the child's screen:229// in one of the child's boxes, where in the box; elsewhere, where in the230// window.231func (g *outGrid) toChild(v *nbview.View, msg tea.MouseMsg) tea.MouseMsg {232 ox, oy := g.origin(v)233 mouse := msg.Mouse()234 x, y := mouse.X-ox, childY(mouse.Y-oy)235 for _, b := range g.boxes() {236 bx, by := b.X+ox, windowY(b.Y)+oy237 if mouse.X >= bx && mouse.Y >= by && len(b.Lines) > 0 && mouse.Y < by+len(b.Lines) && mouse.X < bx+boxWidth(b) {238 x, y = b.X+mouse.X-bx, b.Y+mouse.Y-by239 }240 }241 at := tea.Mouse{X: x, Y: y, Button: mouse.Button, Mod: mouse.Mod}242 switch msg.(type) {243 case tea.MouseClickMsg:244 return tea.MouseClickMsg(at)245 case tea.MouseReleaseMsg:246 return tea.MouseReleaseMsg(at)247 case tea.MouseWheelMsg:248 return tea.MouseWheelMsg(at)249 }250 return tea.MouseMotionMsg(at)251}252253// boxes are the child's boxes, laid out with the screen's room.254func (g *outGrid) boxes() []overlay.Box {255 var out []overlay.Box256 g.roomy(func() { out = g.child.floating() })257 return out258}259260// gridBoxes are the child's boxes (a column's filter, a menu) placed on the261// program's screen over the window.262func (m *Model) gridBoxes() []overlay.Box {263 g, v := m.gridIn(), m.nbView()264 if g == nil || v == nil {265 return nil266 }267 ox, oy := g.origin(v)268 boxes := g.boxes()269 out := make([]overlay.Box, 0, len(boxes))270 for _, b := range boxes {271 b.X, b.Y = max(b.X+ox, 0), max(windowY(b.Y)+oy, 0)272 out = append(out, b)273 }274 return out275}