cat / 012
clone

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

main
012 / internal / ui / bands.go
bands.go 8.4K
1package ui23import (4 "src.rest/cat/012/internal/sheet"5 "src.rest/cat/012/internal/ui/rowtext"6)78// Rows on screen. Each row shown is a band of screen lines: a rule line9// first when a border lies along its top, then as many lines as its10// cells take (rowtext.Shapes), the last row cut off at the bottom of the11// grid. A sheet nothing shapes has a line per row. Scrolling moves a row12// at a time, so the top row always shows from its first line.1314// band is a row's place on screen.15type band struct {16 row int // the row, or divider17 y int // the screen line it starts on18 rule bool // a border line comes first19 shown int // lines of the row's cells on screen20 lines int // lines the row's cells take21}2223// top is the screen line of the band's first line of cells.24func (b band) top() int {25 if b.rule {26 return b.y + 127 }28 return b.y29}3031// end is the screen line after the band.32func (b band) end() int { return b.top() + b.shown }3334// divider marks the line between frozen and scrolling rows.35const divider = -13637// shape returns how row r is laid out.38func (g *grid) shape(r int) rowtext.Shape { return g.shapes.Row(g.sheet, r) }3940// span is how many screen lines row r takes, its rule line included.41func (g *grid) span(r int) int {42 sh := g.shape(r)43 if sh.Rule {44 return sh.Lines + 145 }46 return sh.Lines47}4849// frozenLines counts the screen lines frozen rows take: those the filter50// doesn't hide.51func (g *grid) frozenLines(rows int) int {52 n := 053 for r := range rows {54 if !g.sheet.RowHidden(r) {55 n += g.span(r)56 }57 }58 return n59}6061// scrollLines is how many screen lines the scrolling rows have below the62// frozen ones and their divider.63func (g *grid) scrollLines() int {64 fr, _ := g.frozen()65 if fr == 0 {66 return g.visibleRows()67 }68 return max(g.visibleRows()-g.frozenLines(fr)-1, 1)69}7071// scrollRows is how many scrolling rows fit below the frozen ones and72// their divider, at least one: a page, for Page Down.73func (g *grid) scrollRows() int {74 if !g.sheet.Shaped() {75 return g.scrollLines()76 }77 n := 078 for _, b := range g.bands() {79 if b.row != divider && b.row >= g.top && b.shown == b.lines {80 n++81 }82 }83 return max(n, 1)84}8586// bands lays out the rows shown, top to bottom: frozen rows, the87// divider, then scrolling rows from top, skipping rows the filter hides.88// Rows past the end of the sheet are left off.89func (g *grid) bands() []band {90 n := g.visibleRows()91 out := make([]band, 0, n)92 y, shaped := gridTop, g.sheet.Shaped()93 add := func(r int) bool {94 sh := rowtext.Shape{Lines: 1}95 if shaped {96 sh = g.shape(r)97 }98 b := band{row: r, y: y, rule: sh.Rule, lines: sh.Lines}99 b.shown = min(sh.Lines, gridTop+n-b.top())100 if b.shown <= 0 {101 return false102 }103 out = append(out, b)104 y = b.end()105 return y < gridTop+n106 }107 fr, _ := g.frozen()108 for r := 0; r < fr; r++ {109 if !g.sheet.RowHidden(r) {110 add(r)111 }112 }113 if fr > 0 {114 out = append(out, band{row: divider, y: y, shown: 1, lines: 1})115 y++116 }117 r, ok := g.top, y < gridTop+n118 if g.sheet.RowHidden(r) {119 r, ok = g.sheet.NextShownRow(r, 1)120 }121 for ; ok && add(r); r, ok = g.sheet.NextShownRow(r, 1) {122 }123 return out124}125126// screenRows returns the rows shown, top to bottom, with divider where127// the frozen rows end.128func (g *grid) screenRows() []int {129 bands := g.bands()130 out := make([]int, len(bands))131 for i, b := range bands {132 out[i] = b.row133 }134 return out135}136137// rowAt returns the row on screen line y, its rule line included, or138// false for the divider and lines outside the grid.139func (g *grid) rowAt(y int) (int, bool) {140 for _, b := range g.bands() {141 if y >= b.y && y < b.end() {142 return b.row, b.row != divider143 }144 }145 return 0, false146}147148// bandOf returns the band of row r, or false if it isn't on screen.149func (g *grid) bandOf(r int) (band, bool) {150 for _, b := range g.bands() {151 if b.row == r {152 return b, true153 }154 }155 return band{}, false156}157158// rowY returns the screen line of row r's first line of cells, or false159// if it isn't on screen.160func (g *grid) rowY(r int) (int, bool) {161 b, ok := g.bandOf(r)162 return b.top(), ok163}164165// stepRow moves n visible rows from r (up for negative n), skipping166// hidden rows and stopping at the edges of the sheet.167func (g *grid) stepRow(r, n int) int {168 d := 1169 if n < 0 {170 d, n = -1, -n171 }172 for ; n > 0; n-- {173 next, ok := g.sheet.NextShownRow(r, d)174 if !ok {175 break176 }177 r = next178 }179 return r180}181182// visibleRow returns r, or the nearest row above it the filter doesn't183// hide (the header row always shows), so the active cell stays in the184// data when its row is filtered out.185func (g *grid) visibleRow(r int) int {186 if !g.sheet.RowHidden(r) {187 return r188 }189 if prev := g.stepRow(r, -1); prev != r {190 return prev191 }192 return g.stepRow(r, 1)193}194195// scrollTo moves the viewport the minimum amount needed to show a. Cells196// in the frozen panes are always on screen.197func (g *grid) scrollTo(a sheet.Addr) {198 g.clampView()199 fr, fc := g.frozen()200 if a.Row >= fr {201 if a.Row < g.top {202 g.top = a.Row203 } else {204 // Walk up from a over as many rows as fit; if that passes205 // g.top, a is already on screen.206 r, used, room := a.Row, g.span(a.Row), g.scrollLines()207 for r > g.top {208 prev := g.stepRow(r, -1)209 if prev == r || used+g.span(prev) > room {210 break211 }212 r, used = prev, used+g.span(prev)213 }214 g.top = max(g.top, r)215 }216 }217 if a.Col >= fc {218 if a.Col < g.left {219 g.left = a.Col220 }221 for a.Col >= g.left+g.visibleCols(g.left) {222 g.left++223 }224 }225 g.clampView()226}227228// lineOf returns the screen line where row r starts, counting down (or229// up) from the top of the scrolling rows, as far as limit lines past the230// grid; rows beyond read as limit away. Charts, which float over the231// scrolling rows, are placed with it.232func (g *grid) lineOf(r, limit int) int {233 y := gridTop234 if !g.sheet.Shaped() {235 return clamp(y+r-g.top, -limit, g.height+limit)236 }237 for k := g.top; k < r && y < g.height+limit; k = g.stepRow(k, 1) {238 if next := g.stepRow(k, 1); next == k {239 break240 }241 y += g.span(k)242 }243 for k := g.top; k > r && y > -limit; {244 prev := g.stepRow(k, -1)245 if prev == k {246 break247 }248 k = prev249 y -= g.span(k)250 }251 return y252}253254// rowAtLine is the inverse of lineOf: the row whose lines hold screen255// line y, counting from the top of the scrolling rows, for lines past256// the grid too.257func (g *grid) rowAtLine(y int) int {258 if !g.sheet.Shaped() {259 return max(g.top+y-gridTop, 0)260 }261 r, at := g.top, gridTop262 for at+g.span(r) <= y {263 next := g.stepRow(r, 1)264 if next == r {265 break266 }267 at, r = at+g.span(r), next268 }269 for at > y {270 prev := g.stepRow(r, -1)271 if prev == r {272 break273 }274 r = prev275 at -= g.span(r)276 }277 return r278}279280// cellPos returns the screen position of a visible cell's left edge, on281// its first line of text.282func (g *grid) cellPos(a sheet.Addr) (x, y int) {283 y, _ = g.rowY(a.Row)284 return g.colStart(a.Col), y285}286287// dragTarget maps a position during a drag to a cell, clamping to the288// visible grid, and reports which way to autoscroll when it's outside.289// Dragging from the scrolling area into the frozen panes scrolls back290// toward them, as in Sheets, until the two meet; anchor is where the291// drag started.292func (g *grid) dragTarget(x, y int, anchor sheet.Addr) (a sheet.Addr, dc, dr int) {293 bands := g.bands()294 fr, _ := g.frozen()295 firstRow := g.visibleRow(fr)296 scrolledDown := g.top > firstRow297 var last, bottom int298 for _, b := range bands {299 if b.row != divider {300 last = b.row301 }302 bottom = b.end()303 }304 switch row, ok := g.rowAt(y); {305 case ok && (row >= fr || !scrolledDown || anchor.Row < fr):306 a.Row = row307 case y >= bottom:308 a.Row, dr = last, 1309 case y >= gridTop && !scrolledDown: // the divider, with nothing scrolled310 a.Row = g.top311 case scrolledDown && anchor.Row >= fr:312 a.Row, dr = g.top, -1313 default: // above the grid314 a.Row = bands[0].row315 if scrolledDown {316 dr = -1317 }318 }319 a.Col, dc = g.dragCol(x, anchor)320 return clampAddr(a), dc, dr321}322323// dragCol is dragTarget's column for x.324func (g *grid) dragCol(x int, anchor sheet.Addr) (col, dc int) {325 _, fc := g.frozen()326 scrolledRight := g.left > fc327 cols := g.visibleCols(g.left)328 right := g.colStart(g.left + cols)329 switch c, _, ok := g.colSpan(x); {330 case x >= right:331 return g.left + cols - 1, 1332 case ok && (c >= fc || !scrolledRight || anchor.Col < fc):333 return c, 0334 case x >= g.hdrW() && !scrolledRight:335 return g.left, 0336 case scrolledRight && anchor.Col >= fc:337 return g.left, -1338 }339 // Over the row numbers.340 col = 0341 if fc == 0 {342 col = g.left343 }344 if scrolledRight || fc == 0 && g.left > 0 {345 dc = -1346 }347 return col, dc348}