1package srcview23import (4 "strconv"5 "strings"67 "github.com/charmbracelet/x/ansi"89 "src.rest/cat/012/internal/sheet"10 "src.rest/cat/012/internal/ui/theme"11)1213// Draw draws the view's lines: the column letters, the header row of14// the columns' names, the divider under it, then the rows, the15// scrollbar at the right edge. It asks the host for the rows shown and16// a page either side, so scrolling finds them read.17func (v *View) Draw() []string {18 v.Prefetch()19 lines := v.Lines()20 v.fit()21 v.follow()22 out := []string{v.letters(), v.names(), v.divider()}23 bar := v.scrollbar(lines)24 for i := range lines {25 out = append(out, v.rowLine(v.top+int64(i))+bar[i])26 }27 return out28}2930// shown are the columns on screen.31func (v *View) shown() []int {32 n := v.fits(v.left)33 out := make([]int, 0, n)34 for c := v.left; c < v.left+n && c < v.cols(); c++ {35 out = append(out, c)36 }37 return out38}3940// pad fills what's left of the width after used columns.41func (v *View) pad(used int) string { return strings.Repeat(" ", max(v.width-used, 0)) }4243// letters is the column header: the columns' letters, as a sheet's.44func (v *View) letters() string {45 th := v.h.Theme()46 var b strings.Builder47 used := v.gutter()48 b.WriteString(th.Header.Render(strings.Repeat(" ", used)))49 for _, c := range v.shown() {50 w := v.colWidth(c)51 style := th.Header52 if c == v.col {53 style = th.HeaderActive54 }55 b.WriteString(style.Render(theme.Center(sheet.ColName(c), w)))56 used += w57 }58 return b.String() + th.Header.Render(v.pad(used))59}6061// names is the header row, the sheet's row 1: the columns' names, bold62// and underlined as a table's header.63func (v *View) names() string {64 th := v.h.Theme()65 var b strings.Builder66 b.WriteString(th.RowHeader.Render(theme.PadLeft("1", v.gutter()-1) + " "))67 used := v.gutter()68 cols := v.h.Columns()69 for _, c := range v.shown() {70 w := v.colWidth(c)71 name := ansi.Truncate(cols[c].Name, w-1, "…")72 b.WriteString(th.TableHeader.Render(theme.PadRight(" "+name, w)))73 used += w74 }75 return b.String() + v.pad(used)76}7778// divider is the line under the frozen header row.79func (v *View) divider() string {80 return v.h.Theme().FrozenLine.Render(strings.Repeat("─", v.width))81}8283// rowLine draws row i of the tab: its number, the sheet's row the84// source's row is (under the header), and its cells.85func (v *View) rowLine(i int64) string {86 th := v.h.Theme()87 used := v.gutter()88 if i >= v.rows() {89 return th.RowHeader.Render(strings.Repeat(" ", used)) + v.pad(used+1)90 }91 num, row, ok := v.h.Row(i)92 hdr := th.RowHeader93 if i == v.cur {94 hdr = th.HeaderActive95 }96 label := "…"97 if ok {98 label = strconv.FormatInt(num+2, 10)99 }100 var b strings.Builder101 b.WriteString(hdr.Render(theme.PadLeft(label, used-1) + " "))102 for _, c := range v.shown() {103 w := v.colWidth(c)104 b.WriteString(v.cell(i, c, w, row, ok))105 used += w106 }107 return b.String() + v.pad(used+1)108}109110// cell draws column c of row i, w wide.111func (v *View) cell(i int64, c, w int, row []sheet.LiveCell, ok bool) string {112 th := v.h.Theme()113 base := th.Cell114 if i == v.cur && c == v.col {115 base = th.Pointer116 }117 switch {118 case !ok:119 if c == v.left {120 return th.Muted.Inherit(base).Render(theme.PadRight(" …", w))121 }122 return base.Render(strings.Repeat(" ", w))123 case c >= len(row):124 return base.Render(strings.Repeat(" ", w))125 }126 cell := row[c]127 text, align := sheet.DisplayIn(cell.V, v.format(c, cell), w, v.h.Locale())128 text = ansi.Truncate(text, w-1, "")129 switch align {130 case sheet.AlignRight, sheet.AlignFill:131 text = theme.PadLeft(text, w-1) + " "132 case sheet.AlignCenter:133 text = theme.Center(text, w)134 default:135 text = " " + theme.PadRight(text, w-1)136 }137 if cell.V.Kind == sheet.Error {138 return th.ErrorCell.Inherit(base).Render(text)139 }140 return base.Render(text)141}142143// scrollbar is the scrollbar's column beside each of lines rows: a144// track, and a thumb as long as the share of the rows shown and as far145// down as the window is, drawn with other characters so it reads146// without color.147func (v *View) scrollbar(lines int) []string {148 th := v.h.Theme()149 out := make([]string, lines)150 start, size := v.thumb(lines)151 for i := range out {152 if i >= start && i < start+size {153 out[i] = th.Key.Render("┃")154 } else {155 out[i] = th.Border.Render("│")156 }157 }158 return out159}160161// thumb is where the scrollbar's thumb starts and how long it is, over162// lines lines.163func (v *View) thumb(lines int) (start, size int) {164 n := v.rows()165 if n <= int64(lines) {166 return 0, lines167 }168 size = max(int(int64(lines)*int64(lines)/n), 1)169 room := int64(lines - size)170 last := n - int64(lines)171 return int(v.top * room / max(last, 1)), size172}173174// Hit is what the screen position x, y of the view is: a cell (its row175// and column), the scrollbar, or nothing.176type Hit struct {177 Row int64178 Col int179 Cell bool180 Bar bool181 BarLines int // how many lines the scrollbar has, for ScrollBar182 BarAt int // the line of the scrollbar hit183}184185// HitAt says what x, y (from the view's first line) is.186func (v *View) HitAt(x, y int) Hit {187 lines := v.Lines()188 if y < firstRow || y >= firstRow+lines {189 return Hit{}190 }191 if x == v.width-1 {192 return Hit{Bar: true, BarLines: lines, BarAt: y - firstRow}193 }194 i := v.top + int64(y-firstRow)195 if i >= v.rows() {196 return Hit{}197 }198 used := v.gutter()199 for _, c := range v.shown() {200 if w := v.colWidth(c); x >= used && x < used+w {201 return Hit{Row: i, Col: c, Cell: true}202 }203 used += v.colWidth(c)204 }205 return Hit{}206}207208// ScrollBar moves the window to where line at of a scrollbar of lines209// lines stands for: a click on the track, or the thumb dragged there.210func (v *View) ScrollBar(at, lines int) {211 n := v.rows() - int64(v.Lines())212 if n <= 0 || lines <= 1 {213 return214 }215 v.ScrollTo(int64(at) * n / int64(lines-1))216}217218// DragBar moves the window to where the scrollbar's line at y (from the219// view's first line) stands for, as the thumb is dragged.220func (v *View) DragBar(y int) {221 lines := v.Lines()222 v.ScrollBar(min(max(y-firstRow, 0), lines-1), lines)223}224225// ColX is the screen column column col starts at, from the view's226// left edge: the row numbers' width when it isn't shown.227func (v *View) ColX(col int) int {228 x := v.gutter()229 for _, c := range v.shown() {230 if c == col {231 return x232 }233 x += v.colWidth(c)234 }235 return v.gutter()236}