cat / 012
clone

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

main
012 / internal / sheet / rowlayout.go
rowlayout.go 8.7K
1package sheet23import (4 "bufio"5 "maps"6 "slices"7 "strconv"89 "src.rest/cat/012/internal/formula"10)1112// Row layout: what makes a row of the grid more than one line of text.13// A row is as tall as its height, in lines, when one is set by hand14// (Format > Row height, or dragging the row's border), kept per sheet as15// column widths are; otherwise the grid grows it to fit the text its16// cells wrap. A horizontal border takes a line of its own above the row17// it tops. Cells whose own style wraps text or draws borders are indexed18// by row (shapers), so the grid measures the rows it shows at the cost19// of those cells, and a sheet without any costs nothing.2021// MaxRowHeight caps a row's height in lines.22const MaxRowHeight = 502324// shapes reports whether a style changes the layout of its row: text25// that wraps, or borders.26func (st Style) shapes() bool { return st.Wrap == WrapOn || !st.Borders.IsZero() }2728// shaperIndex holds the columns of each row whose cells' own styles29// shape it, and whether any line format does, as of a version.30type shaperIndex struct {31 rows map[int]map[int]struct{}32 linesAt uint64 // the version the rest was worked out at, plus one33 lineShape bool // a line format wraps or draws borders34 colRules bool // a column's format draws a top or bottom edge35}3637// trackShape keeps the index as the cell at a becomes c.38func (s *Sheet) trackShape(a Addr, c *Cell) {39 _, was, _ := s.cells.look(a)40 had, has := was.shapes(), c != nil && c.Style.shapes()41 switch {42 case has && !had:43 if s.shapers.rows == nil {44 s.shapers.rows = map[int]map[int]struct{}{}45 }46 row := s.shapers.rows[a.Row]47 if row == nil {48 row = map[int]struct{}{}49 s.shapers.rows[a.Row] = row50 }51 row[a.Col] = struct{}{}52 case had && !has:53 row := s.shapers.rows[a.Row]54 delete(row, a.Col)55 if len(row) == 0 {56 delete(s.shapers.rows, a.Row)57 }58 }59}6061// linesShape reports whether any line format wraps or draws borders.62func (s *Sheet) linesShape() bool {63 if x := &s.shapers; x.linesAt != s.version+1 {64 x.linesAt, x.lineShape, x.colRules = s.version+1, s.lines.sheet.Style.shapes(), false65 for _, l := range s.lines.cols {66 x.lineShape = x.lineShape || l.Style.shapes()67 x.colRules = x.colRules || l.Style.Borders.horizontal()68 }69 for _, l := range s.lines.rows {70 x.lineShape = x.lineShape || l.Style.shapes()71 }72 }73 return s.shapers.lineShape74}7576// horizontal reports whether b draws its top or bottom edge.77func (b Borders) horizontal() bool { return b.Top() != LineNone || b.Bottom() != LineNone }7879// Shaped reports whether any row may be other than one line of text80// under the one above: something wraps or draws borders, a row has a81// height of its own, or cells are merged. The grid lays out a sheet that82// isn't shaped a line per row without asking more.83func (s *Sheet) Shaped() bool {84 return len(s.shapers.rows) > 0 || len(s.heights) > 0 || len(s.view.merges) > 0 || s.linesShape()85}8687// Version changes whenever anything the grid draws may have: cells,88// their values and formats, widths, heights and merges.89func (s *Sheet) Version() uint64 { return s.version }9091// shapersIn calls fn with the columns of row whose cells may wrap or have92// borders: those indexed, and when line formats shape, those of93// formatted columns and, for a shaping row or sheet format, every cell94// the row holds.95func (s *Sheet) shapersIn(row int, fn func(col int)) {96 for c := range s.shapers.rows[row] {97 fn(c)98 }99 if !s.linesShape() {100 return101 }102 if s.lines.sheet.Style.shapes() || s.lines.rows[row].Style.shapes() {103 for a := range s.cells.inRange(rowRect(row, row)) {104 if _, seen := s.shapers.rows[row][a.Col]; !seen {105 fn(a.Col)106 }107 }108 return109 }110 for c, l := range s.lines.cols {111 if _, seen := s.shapers.rows[row][c]; !seen && l.Style.shapes() {112 fn(c)113 }114 }115}116117// BorderedIn returns the blank cells of r whose own formatting draws118// borders, in no particular order, at the cost of the cells that wrap or119// draw borders.120func (s *Sheet) BorderedIn(r Rect) []Addr {121 var out []Addr122 for row, cols := range s.shapers.rows {123 if row < r.From.Row || row > r.To.Row {124 continue125 }126 for c := range cols {127 a := Addr{Col: c, Row: row}128 if _, st, _ := s.cells.look(a); r.Contains(a) && !s.cells.filledAt(a) && !st.Borders.IsZero() {129 out = append(out, a)130 }131 }132 }133 return out134}135136// WrappedIn returns the columns of row whose cells wrap text they hold,137// outside merged cells, in no particular order.138func (s *Sheet) WrappedIn(row int) []int {139 var out []int140 s.shapersIn(row, func(c int) {141 a := Addr{Col: c, Row: row}142 if !s.cells.filledAt(a) || s.CellStyle(a).Wrap != WrapOn {143 return144 }145 if _, merged := s.MergeAt(a); !merged {146 out = append(out, c)147 }148 })149 return out150}151152// RuleAbove reports whether a border lies on the edge above row: the top153// of one of its cells or the bottom of one in the row before, or a154// column's, row's or the sheet's format drawing either.155func (s *Sheet) RuleAbove(row int) bool {156 if s.linesShape() && (s.shapers.colRules || s.lines.sheet.Style.Borders.horizontal() ||157 s.lines.rows[row].Style.Borders.Top() != LineNone || row > 0 && s.lines.rows[row-1].Style.Borders.Bottom() != LineNone) {158 return true159 }160 found := false161 s.shapersIn(row, func(c int) {162 found = found || s.CellStyle(Addr{Col: c, Row: row}).Borders.Top() != LineNone163 })164 if row > 0 && !found {165 s.shapersIn(row-1, func(c int) {166 found = found || s.CellStyle(Addr{Col: c, Row: row - 1}).Borders.Bottom() != LineNone167 })168 }169 return found170}171172// RowHeight returns the height set for row by hand, in lines, and false173// when it has none and fits its contents.174func (s *Sheet) RowHeight(row int) (int, bool) {175 h, ok := s.heights[row]176 return h, ok177}178179// SetRowHeight sets rows from..to to h lines, as one undo step; h 0180// has them fit their contents again, as Sheets' Fit to data. Heights are181// kept row by row, so a range of more than materializeLimit rows (whole182// columns) stops at the last row holding a cell.183func (s *Sheet) SetRowHeight(from, to, h int) {184 label := "row height"185 if h <= 0 {186 label = "fit rows to data"187 }188 if to-from >= materializeLimit {189 used, _ := s.cells.bounds(rowRect(from, to))190 to = max(from, used.To.Row)191 }192 s.change(label, rowRect(from, to), func() {193 for r := from; r <= to; r++ {194 s.setHeight(r, h)195 }196 })197}198199// ColsWidth is the width of columns from..to together, at the cost of200// the columns with a width of their own.201func (s *Sheet) ColsWidth(from, to int) int {202 w := (to - from + 1) * DefaultWidth203 for c, cw := range s.widths {204 if c >= from && c <= to {205 w += cw - DefaultWidth206 }207 }208 return w209}210211// Heights returns the rows that have a height set by hand.212func (s *Sheet) Heights() map[int]int { return maps.Clone(s.heights) }213214// LoadRowHeight sets a row's height as an importer does, without215// recording undo.216func (s *Sheet) LoadRowHeight(row, h int) {217 if row >= 0 && row < MaxRows {218 s.setHeight(row, h)219 }220}221222func (s *Sheet) setHeight(row, h int) {223 h = min(h, MaxRowHeight)224 if old, ok := s.heights[row]; ok == (h > 0) && old == h {225 return226 }227 s.recordHeight(row)228 s.version++229 if h <= 0 {230 delete(s.heights, row)231 return232 }233 if s.heights == nil {234 s.heights = map[int]int{}235 }236 s.heights[row] = h237}238239// rowKey is a row of a sheet, for undo.240type rowKey struct {241 s *Sheet242 row int243}244245// recordHeight saves row's height before its first change in the open246// step; 0 stands for none.247func (s *Sheet) recordHeight(row int) {248 st := s.wb.hist.open249 if st == nil {250 return251 }252 k := rowKey{s, row}253 if _, seen := st.heights[k]; !seen {254 st.keepHeight(k, s.heights[row])255 }256}257258// keepHeight records row k's height h as it was before the step.259func (st *step) keepHeight(k rowKey, h int) {260 if st.heights == nil {261 st.heights = map[rowKey]int{}262 }263 st.heights[k] = h264}265266// shiftHeights moves row heights along with inserted or deleted rows.267func (s *Sheet) shiftHeights(sp formula.Span) {268 next := map[int]int{}269 for r, h := range s.heights {270 if to, ok := sp.Point(r); ok {271 next[to] = h272 }273 }274 for _, r := range slices.Collect(maps.Keys(s.heights)) {275 if _, ok := next[r]; !ok {276 s.setHeight(r, 0)277 }278 }279 for r, h := range next {280 s.setHeight(r, h)281 }282}283284// heightsIn is the heights of rows lo..hi, by offset from lo.285func (s *Sheet) heightsIn(lo, hi int) map[int]int {286 var out map[int]int287 for r, h := range s.heights {288 if r >= lo && r <= hi {289 if out == nil {290 out = map[int]int{}291 }292 out[r-lo] = h293 }294 }295 return out296}297298// writeHeights writes the "heights" field: rows by number, in lines.299func (s *Sheet) writeHeights(b *bufio.Writer, indent string) {300 if len(s.heights) == 0 {301 return302 }303 b.WriteString(indent + `"heights": {`)304 for i, r := range slices.Sorted(maps.Keys(s.heights)) {305 if i > 0 {306 b.WriteString(", ")307 }308 b.WriteString(strconv.Quote(strconv.Itoa(r+1)) + ": " + strconv.Itoa(s.heights[r]))309 }310 b.WriteString("},\n")311}