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 / rowtext / shape.go
shape.go 4.0K
1package rowtext23import (4 "strings"56 "github.com/charmbracelet/x/ansi"78 "src.rest/cat/012/internal/sheet"9)1011// Wrap breaks text into lines at most w columns wide, as Sheets wraps a12// cell: at spaces where it can, inside a word longer than a line, and at13// every line break the text holds. It returns at least one line.14func Wrap(text string, w int) []string {15 w = max(w, 1)16 var out []string17 for para := range strings.SplitSeq(text, "\n") {18 out = wrapLine(out, para, w)19 }20 return out21}2223// wrapLine appends the lines of one paragraph to out.24func wrapLine(out []string, para string, w int) []string {25 line, lw := "", 026 for word := range strings.FieldsSeq(para) {27 ww := ansi.StringWidth(word)28 switch {29 case lw > 0 && lw+1+ww <= w:30 line, lw = line+" "+word, lw+1+ww31 continue32 case lw > 0:33 out = append(out, line)34 }35 for ww > w { // a word longer than a line breaks where it must36 c := textCutter{s: word}37 head := c.cut(0, w)38 if head == "" { // a character wider than the line39 n, _ := nextCluster(word)40 head = word[:n]41 }42 out = append(out, head)43 word = word[len(head):]44 ww = ansi.StringWidth(word)45 }46 line, lw = word, ww47 }48 return append(out, line)49}5051// Shape is how a row is laid out: whether a border line comes above it,52// and how many lines its cells take.53type Shape struct {54 Rule bool55 Lines int56}5758// Shapes works out the shape of rows as the grid asks for them, and59// keeps them until the sheet changes.60type Shapes struct {61 s *sheet.Sheet62 version uint6463 rows map[int]Shape64}6566// Row returns the shape of row on s: its height when one is set, or67// enough lines for the text its cells wrap, and a rule line when a68// border lies along its top. Rows of a sheet that nothing shapes are a69// line each.70func (x *Shapes) Row(s *sheet.Sheet, row int) Shape {71 if !s.Shaped() {72 return Shape{Lines: 1}73 }74 if x.s != s || x.version != s.Version() || x.rows == nil {75 x.s, x.version, x.rows = s, s.Version(), map[int]Shape{}76 }77 if sh, ok := x.rows[row]; ok {78 return sh79 }80 sh := Shape{Rule: s.RuleAbove(row), Lines: fit(s, row)}81 x.rows[row] = sh82 return sh83}8485// fit is how many lines row takes: its height, or enough for the text86// it wraps.87func fit(s *sheet.Sheet, row int) int {88 if h, ok := s.RowHeight(row); ok {89 return h90 }91 n := 192 for _, c := range s.WrappedIn(row) {93 a := sheet.Addr{Col: c, Row: row}94 v := s.Value(a)95 if v.Kind != sheet.Text {96 continue97 }98 n = max(n, len(Wrap(v.Str, s.ColWidth(c)-2)))99 }100 return min(n, sheet.MaxRowHeight)101}102103// Merged lays out a merged cell m over its columns from lo to hi (those104// on screen): its top-left cell's value when show is set, on the line of105// the merge that shows it, centered unless that cell is aligned and cut106// to the merge's width; blank otherwise.107func Merged(s *sheet.Sheet, m sheet.Rect, lo, hi int, show bool) []Span {108 x := make([]int, hi-lo+2) // where each column starts, from the merge's first109 x[0] = s.ColsWidth(m.From.Col, lo-1)110 for c := lo; c <= hi; c++ {111 x[c-lo+1] = x[c-lo] + s.ColWidth(c)112 }113 w := s.ColsWidth(m.From.Col, m.To.Col)114 out := make([]Span, hi-lo+1)115 for c := lo; c <= hi; c++ {116 out[c-lo] = Span{Trail: s.ColWidth(c), Owner: m.From.Col}117 }118 cell := s.Cell(m.From)119 if !show || cell.Blank() {120 return out121 }122 f := s.DisplayFormat(m.From)123 st := s.CellStyle(m.From)124 text, align := sheet.Display(cell.Value, f, w)125 if st.Align != sheet.AlignAuto && align != sheet.AlignFill {126 align = st.Align127 } else if align != sheet.AlignFill {128 align = sheet.AlignCenter129 }130 text = ansi.Truncate(text, max(w-2, 0), "")131 tw := ansi.StringWidth(text)132 start := (w - tw) / 2133 switch align {134 case sheet.AlignLeft:135 start = 1136 case sheet.AlignRight:137 start = w - 1 - tw138 case sheet.AlignFill:139 start = 0140 }141 cut := textCutter{s: text}142 for c := lo; c <= hi; c++ {143 kx0, kx1 := x[c-lo], x[c-lo+1]144 sp := Span{Trail: kx1 - kx0, Style: st, Owner: m.From.Col}145 if seg0, seg1 := max(start, kx0), min(start+tw, kx1); seg1 > seg0 {146 sp.Lead, sp.Text, sp.Trail = seg0-kx0, cut.cut(seg0-start, seg1-start), kx1-seg1147 }148 out[c-lo] = sp149 }150 return out151}