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 / srcview / view.go
view.go 6.4K
1// Package srcview is a linked source's tab: a window of the source's2// rows drawn as a sheet with a frozen header row, the row numbers and a3// scrollbar over the whole source, however many millions of rows it4// has. The rows come from the Host a page at a time as the window moves5// (paged.Pages); rows on their way show as a dim ellipsis. The view6// owns the active cell and the scroll; the UI owns the rest.7package srcview89import (10 "strconv"1112 "src.rest/cat/012/internal/locale"13 "src.rest/cat/012/internal/sheet"14 "src.rest/cat/012/internal/ui/theme"15)1617// Column is one of the source's columns.18type Column struct {19 Name string20 Format sheet.Format21}2223// Host is what the view needs of the UI.24type Host interface {25 Theme() *theme.Theme26 Locale() *locale.Locale27 // Columns are the source's columns, left to right.28 Columns() []Column29 // Rows counts the rows the tab shows, false while they're being30 // counted (a sort or a filter being applied).31 Rows() (int64, bool)32 // Row is the tab's row i: its number in the source, counting from33 // 0, and its values, false while its page is on its way.34 Row(i int64) (int64, []sheet.LiveCell, bool)35 // Want asks for the rows from through to.36 Want(from, to int64)37}3839// View is a source's tab.40type View struct {41 h Host42 width, height int // from the column letters down to the last row line43 top, cur int64 // the first row shown, the active row: of the tab's rows44 col, left int // the active column, the first shown45 widths []int46 fitted bool // widths fitted to the first rows read47}4849// New is a view of what h shows.50func New(h Host) *View { return &View{h: h} }5152// The view's lines: the column letters, the header row of names, the53// divider under it, then the rows.54const (55 letterLine = iota56 namesLine57 dividerLine58 firstRow59)6061// minWidth and maxWidth bound a column's fitted width.62const (63 minWidth = 664 maxWidth = 3065)6667// Resize gives the view width columns and height lines.68func (v *View) Resize(width, height int) {69 v.width, v.height = width, height70 v.clamp()71}7273// Lines are how many rows show.74func (v *View) Lines() int { return max(v.height-firstRow, 1) }7576// Active is the active cell: its row among the tab's, its column.77func (v *View) Active() (int64, int) { return v.cur, v.col }7879// Top is the first row shown.80func (v *View) Top() int64 { return v.top }8182// Cell is the active cell's number in the source and value, with false83// while its row is on its way.84func (v *View) Cell() (int64, sheet.LiveCell, bool) {85 num, row, ok := v.h.Row(v.cur)86 if !ok || v.col >= len(row) {87 return num, sheet.LiveCell{}, ok88 }89 return num, row[v.col], true90}9192// Reset goes back to the first row, as when the rows are sorted or93// filtered again.94func (v *View) Reset() {95 v.top, v.cur = 0, 096 v.clamp()97}9899// rows counts the rows, 0 while they're being counted.100func (v *View) rows() int64 {101 n, _ := v.h.Rows()102 return n103}104105// cols counts the columns.106func (v *View) cols() int { return len(v.h.Columns()) }107108// Move moves the active cell by rows and cols, keeping it in the109// window.110func (v *View) Move(rows int64, cols int) {111 v.cur += rows112 v.col += cols113 v.clamp()114 v.follow()115}116117// MoveTo makes row and col the active cell, row -1 meaning the last.118func (v *View) MoveTo(row int64, col int) {119 if row < 0 {120 row = v.rows() - 1121 }122 if col < 0 {123 col = v.cols() - 1124 }125 v.cur, v.col = row, col126 v.clamp()127 v.follow()128}129130// Scroll moves the window d rows, leaving the active cell where it is131// unless it would leave the window.132func (v *View) Scroll(d int64) {133 v.top += d134 v.clamp()135 if v.cur < v.top {136 v.cur = v.top137 }138 if last := v.top + int64(v.Lines()) - 1; v.cur > last {139 v.cur = last140 }141 v.clamp()142}143144// ScrollTo shows the rows from top on.145func (v *View) ScrollTo(top int64) { v.Scroll(top - v.top) }146147// clamp keeps the active cell and the window in the rows and columns.148func (v *View) clamp() {149 n := v.rows()150 v.cur = max(min(v.cur, n-1), 0)151 v.top = max(min(v.top, n-int64(v.Lines())), 0)152 v.col = max(min(v.col, v.cols()-1), 0)153 v.left = max(min(v.left, v.col), 0)154}155156// follow scrolls the active cell into view.157func (v *View) follow() {158 lines := int64(v.Lines())159 switch {160 case v.cur < v.top:161 v.top = v.cur162 case v.cur >= v.top+lines:163 v.top = v.cur - lines + 1164 }165 for v.col > v.left && v.col >= v.left+v.fits(v.left) {166 v.left++167 }168 v.clamp()169}170171// gutter is the width of the row numbers: the widest a row shows as,172// counting the header's row 1, and room.173func (v *View) gutter() int {174 return max(6, len(strconv.FormatInt(v.rows()+1, 10))+2)175}176177// width is column c's width.178func (v *View) colWidth(c int) int {179 if c < len(v.widths) {180 return v.widths[c]181 }182 return sheet.DefaultWidth183}184185// fits is how many columns from left fit beside the row numbers and the186// scrollbar, at least one.187func (v *View) fits(left int) int {188 room := v.width - v.gutter() - 1189 n := 0190 for c := left; c < v.cols(); c++ {191 if room -= v.colWidth(c); room < 0 {192 break193 }194 n++195 }196 return max(n, 1)197}198199// fit sizes the columns to their names and the rows first read, once.200func (v *View) fit() {201 cols := v.h.Columns()202 if v.fitted || len(cols) == 0 {203 return204 }205 widths := make([]int, len(cols))206 for c, col := range cols {207 widths[c] = max(len(sheet.ColName(c)), len([]rune(col.Name)))208 }209 _, fitted := v.h.Rows() // fitted again once the rows are counted210 for i := v.top; i < v.top+int64(v.Lines()) && i < v.rows(); i++ {211 _, row, ok := v.h.Row(i)212 if !ok {213 fitted = false // fitted again once the window's rows are here214 break215 }216 for c, cell := range row {217 if c < len(widths) && cell.V.Kind != sheet.Empty {218 text := sheet.FormatTextIn(cell.V, v.format(c, cell), v.h.Locale())219 widths[c] = max(widths[c], len([]rune(text)))220 }221 }222 }223 for c := range widths {224 widths[c] = min(max(widths[c]+2, minWidth), maxWidth)225 }226 v.widths, v.fitted = widths, fitted227}228229// Prefetch asks for the rows shown and a window's worth either side, so230// scrolling finds them read.231func (v *View) Prefetch() {232 v.clamp()233 lines := int64(v.Lines())234 v.h.Want(v.top-lines, v.top+2*lines)235}236237// format is what a cell of column c shows in: its own, or its column's.238func (v *View) format(c int, cell sheet.LiveCell) sheet.Format {239 if !cell.F.IsZero() {240 return cell.F241 }242 if cols := v.h.Columns(); c < len(cols) {243 return cols[c].Format244 }245 return sheet.Format{}246}247248// Refit sizes the columns again once rows are read, as when the source249// is read again.250func (v *View) Refit() { v.fitted = false }