cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / internal / ui / chartsel.go
chartsel.go 6.6K
1package ui23import (4 tea "charm.land/bubbletea/v2"5 "github.com/charmbracelet/x/ansi"67 "src.rest/cat/012/internal/sheet"8 "src.rest/cat/012/internal/ui/overlay"9)1011// chartSel is a selected chart. Arrows move it a cell at a time, Shift12// and arrows resize it, Enter edits it, Del deletes it, and Space held13// shows it across the grid (keyboard.go); the mouse drags it or its14// corner. Other keys deselect it and act as usual.15type chartSel struct {16 m chartHost // the model, through what a selected chart needs of it17 i int18 drag int // chartDragNone, chartDragMove or chartDragResize19 grabX int // where the chart was grabbed, relative to its corner20 grabY int21 preview *sheet.Chart // the chart while being dragged, or held open22 zoom bool // Space is held: preview is the chart across the grid23}2425func (s *chartSel) Indicator() string { return "CHART" }2627func (s *chartSel) Layout() []overlay.Box {28 return nil // drawn with the other charts, see chartBoxes29}3031func (s *chartSel) chart() (sheet.Chart, bool) {32 charts := s.m.sheetShown().Charts()33 if s.i < 0 || s.i >= len(charts) {34 return sheet.Chart{}, false35 }36 return charts[s.i], true37}3839// shown is the chart as drawn: where it's being dragged, if it is.40func (s *chartSel) shown() (sheet.Chart, bool) {41 if s.preview != nil {42 return *s.preview, true43 }44 return s.chart()45}4647func (s *chartSel) Key(k tea.KeyPressMsg) tea.Cmd {48 m := s.m49 c, ok := s.chart()50 if !ok {51 m.closeOverlay()52 return nil53 }54 if s.holdKey(m, c, k) {55 return nil56 }57 moved := c58 switch k.String() {59 case "esc":60 m.closeOverlay()61 return nil62 case "enter":63 return m.runCommand("chart.edit")64 case "delete", "backspace":65 return m.runCommand("chart.delete")66 case "tab":67 m.selectChart((s.i + 1) % len(m.sheetShown().Charts()))68 return nil69 case "up":70 moved.At.Row--71 case "down":72 moved.At.Row++73 case "left":74 moved.At.Col--75 case "right":76 moved.At.Col++77 case "shift+up":78 moved.H--79 case "shift+down":80 moved.H++81 case "shift+left":82 moved.W -= 283 case "shift+right":84 moved.W += 285 default:86 m.closeOverlay()87 return m.passKey(k)88 }89 label := "move chart"90 if moved.W != c.W || moved.H != c.H {91 label = "resize chart"92 }93 s.place(c, moved, label)94 m.showChart(s.i)95 return nil96}9798// place puts the chart at to, from where it was, as one undo step, and99// records it.100func (s *chartSel) place(from, to sheet.Chart, label string) {101 sh := s.m.sheetShown()102 sh.SetChart(s.i, to, label)103 s.m.syncChanged()104 if now, ok := s.chart(); ok {105 s.m.recordChart("chart.edit", s.i, from, now, true)106 }107}108109// press starts a drag at x, y: the corner resizes, anywhere else moves.110func (s *chartSel) press(x, y int) {111 c, ok := s.chart()112 if !ok {113 return114 }115 cx, cy := s.m.chartScreen(c)116 s.grabX, s.grabY = x-cx, y-cy117 s.drag = chartDragMove118 if x >= cx+c.W-2 && y == cy+c.H-1 {119 s.drag = chartDragResize120 }121 s.preview = &c122}123124func (s *chartSel) Mouse(e overlay.MouseEvent) tea.Cmd {125 if e.Kind != overlay.MouseMotion {126 s.unzoom()127 }128 switch e.Kind {129 case overlay.MousePress:130 return s.pressAt(e)131 case overlay.MouseMotion:132 return s.motion(e)133 case overlay.MouseRelease:134 s.release()135 case overlay.MouseWheel:136 return s.m.passMouse(e)137 }138 return nil139}140141// pressAt selects the chart pressed and starts dragging it, or opens its142// menu; pressing off the charts deselects and acts as usual.143func (s *chartSel) pressAt(e overlay.MouseEvent) tea.Cmd {144 m := s.m145 i := m.chartAt(e.X, e.Y)146 switch {147 case i < 0:148 m.closeOverlay()149 return m.passMouse(e)150 case e.Button == tea.MouseRight:151 m.selectChart(i)152 m.showChartMenu(e.X, e.Y+1)153 case e.Button == tea.MouseLeft:154 if i != s.i {155 s = m.selectChart(i)156 }157 s.press(e.X, e.Y)158 }159 return nil160}161162// motion moves or resizes the chart being dragged, or else sets the163// pointer shape for what's under the mouse.164func (s *chartSel) motion(e overlay.MouseEvent) tea.Cmd {165 m := s.m166 if s.drag == chartDragNone || s.preview == nil {167 return m.setShape(s.hoverShape(e.X, e.Y))168 }169 p := *s.preview170 if s.drag == chartDragResize {171 cx, cy := m.chartScreen(p)172 p.W = clamp(e.X-cx+1, sheet.MinChartW, sheet.MaxChartW)173 p.H = clamp(e.Y-cy+1, sheet.MinChartH, sheet.MaxChartH)174 } else {175 p.At = m.chartCellAt(e.X-s.grabX, e.Y-s.grabY)176 }177 s.preview = &p178 return nil179}180181// hoverShape is the pointer shape at x, y: a move cursor over charts,182// and a resize one over the selected chart's corner.183func (s *chartSel) hoverShape(x, y int) string {184 i := s.m.chartAt(x, y)185 if i < 0 {186 return "default"187 }188 if c, ok := s.shown(); ok && i == s.i {189 cx, cy := s.m.chartScreen(c)190 if x >= cx+c.W-2 && y == cy+c.H-1 {191 return "nwse-resize"192 }193 }194 return "move"195}196197// release drops the chart being dragged where it is, as one undo step.198func (s *chartSel) release() {199 c, ok := s.chart()200 if ok && s.preview != nil && s.drag != chartDragNone && *s.preview != c {201 label := "move chart"202 if s.drag == chartDragResize {203 label = "resize chart"204 }205 s.place(c, *s.preview, label)206 }207 s.drag, s.preview = chartDragNone, nil208}209210// chartCellAt returns the cell a chart's corner snaps to when dragged to211// screen position x, y, which may be past the grid's edges.212func (g *grid) chartCellAt(x, y int) sheet.Addr {213 a := sheet.Addr{Row: g.rowAtLine(y), Col: g.left}214 switch {215 case x >= g.hdrW():216 if col, _, ok := g.colSpan(x); ok {217 a.Col = col218 } else {219 a.Col = g.left + g.visibleCols(g.left) - 1220 }221 default:222 for cx := g.hdrW(); cx > x && a.Col > 0; {223 a.Col--224 cx -= g.sheet.ColWidth(a.Col)225 }226 }227 return clampAddr(a)228}229230func (s *chartSel) Status() (string, string) {231 th := s.m.styles()232 width, _ := s.m.size()233 c, ok := s.chart()234 if !ok {235 return "", ""236 }237 desc := th.Muted.Render("Chart of " + c.Data.String())238 if s.zoom {239 return desc, th.KeyHints("Space", "let go to go back")240 }241 pairs := []string{"Enter", "edit", "Del", "delete", "Arrows", "move", "Shift+arrows", "resize", "Esc", "done"}242 for {243 keys := th.KeyHints(pairs...)244 switch {245 case ansi.StringWidth(desc)+3+ansi.StringWidth(keys) <= width:246 return desc, keys247 case desc != "":248 desc = ""249 case len(pairs) > 4:250 pairs = append(pairs[:len(pairs)-4], pairs[len(pairs)-2:]...) // keep Esc251 default:252 return "", keys253 }254 }255}256257// ContextLine says what's selected and how to change it.258func (s *chartSel) ContextLine() (string, string) {259 th := s.m.styles()260 c, ok := s.chart()261 if !ok {262 return "", ""263 }264 how := " drag to move, drag the corner to resize"265 if s.m.holdsKeys() { // Space held enlarges the chart: keyboard.go266 how += ", hold Space to zoom"267 }268 return th.Key.Render(c.Type.Title()+" chart") + th.Muted.Render(" of ") + c.Data.String() +269 th.Muted.Render(how), ""270}271272const (273 chartDragNone = iota274 chartDragMove275 chartDragResize276)