cat / 012
clone

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

Move the row text layout into internal/ui/rowtext

Dami committed 510f080
Laying out a row's cell text reads only the sheet, and it is the part
of drawing a frame that costs the most, so it becomes a leaf package
with its test, where it can be measured and changed on its own.
commit
510f0801ceb7e08fb7addf81495a0b9a612cdede
parent
c505812f20768b60cdeb4ea48da5236d82218aee
author
Dami <dami.etoile@gmail.com>
4 files changed, +53 -46
Minternal/ui/links.go21++++++------
Rinternal/ui/rowtext.go → internal/ui/rowtext/rowtext.go51++++++++++++++++--------------
Rinternal/ui/view_test.go → internal/ui/rowtext/rowtext_test.go2+-
Minternal/ui/view.go25++++++++-------
M internal/ui/links.go +11 -10
@@ -4,6 +4,7 @@ import (
44 "charm.land/lipgloss/v2"55 66 "github.com/FelineStateMachine/012/internal/sheet"7+ "github.com/FelineStateMachine/012/internal/ui/rowtext"78 "github.com/FelineStateMachine/012/internal/ui/theme"89 )910
@@ -14,30 +15,30 @@ import (
1415 1516 // decorate marks a span's text as a link or an error, from the cell the1617 // text belongs to.17-func (g *grid) decorate(sp *span, row int) {18- if sp.text == "" {18+func (g *grid) decorate(sp *rowtext.Span, row int) {19+ if sp.Text == "" {1920 return2021 }21- a := sheet.Addr{Col: sp.owner, Row: row}22+ a := sheet.Addr{Col: sp.Owner, Row: row}2223 switch v := g.sheet.Value(a); {2324 case v.Kind == sheet.Error && !sheet.IsPending(v):24- sp.error = true25+ sp.Error = true2526 case v.Kind == sheet.Text:26- sp.link = g.sheet.Link(a)27+ sp.Link = g.sheet.Link(a)2728 }2829 }2930 3031 // spanStyle is the style of a span's text on base: the cell's text style,3132 // then the link or error mark. On a colored role (the pointer, the3233 // selection) a link keeps the role's colors and adds its underline.33-func spanStyle(th *theme.Theme, base lipgloss.Style, sp span) lipgloss.Style {34- st := sp.style34+func spanStyle(th *theme.Theme, base lipgloss.Style, sp rowtext.Span) lipgloss.Style {35+ st := sp.Style3536 st.Align = sheet.AlignAuto3637 s := th.Text(base, st)3738 switch {38- case sp.link != "":39- s = s.Inherit(th.Link).Hyperlink(sp.link)40- case sp.error:39+ case sp.Link != "":40+ s = s.Inherit(th.Link).Hyperlink(sp.Link)41+ case sp.Error:4142 s = s.Inherit(th.ErrorMark)4243 }4344 return s
R internal/ui/rowtext.go → internal/ui/rowtext/rowtext.go +28 -23
@@ -1,4 +1,9 @@
1-package ui1+// Package rowtext lays out the text of one row of cells across the2+// columns on screen, as Sheets does: values formatted and aligned, text3+// running on into blank neighbors, cut to column boundaries in a single4+// pass. It reads the sheet and nothing else, so drawing the grid can be5+// measured and tested on its own.6+package rowtext27 38 import (49 "strings"
@@ -8,21 +13,21 @@ import (
813 "github.com/FelineStateMachine/012/internal/sheet"914 )1015 11-// span is what one grid column shows in a row: blank columns, the text,16+// Span is what one grid column shows in a row: blank columns, the text,1217 // blank columns. Only the text takes the owning cell's text style, so an1318 // underline doesn't run into the padding.14-type span struct {15- lead int16- text string17- trail int18- style sheet.Style19- owner int // column of the cell the text belongs to20-21- link string // the owner's link target, drawn as a hyperlink22- error bool // the owner shows an error: its text gets the error mark19+type Span struct {20+ Lead int21+ Text string22+ Trail int23+ Style sheet.Style24+ Owner int // column of the cell the text belongs to25+26+ Link string // the owner's link target, drawn as a hyperlink27+ Error bool // the owner shows an error: its text gets the error mark2328 }2429 25-// rowText lays out ncols columns of row from lo, each span exactly its30+// Layout lays out ncols columns of row from lo, each span exactly its2631 // column's width. Text may run in from columns between minCol and maxCol,2732 // so it stops at the frozen columns' divider. Values are formatted with2833 // their cell's number format and aligned as Sheets does: numbers right,
@@ -31,10 +36,10 @@ type span struct {
3136 // left-aligned, to the left when right-aligned, both ways when centered.3237 // It can come from cells outside the viewport, so the scan starts at the3338 // nearest filled cell on each side.34-func rowText(s *sheet.Sheet, row, lo, ncols, minCol, maxCol int) []span {35- l := rowLayout{s: s, row: row, lo: lo, hi: lo + ncols - 1, out: make([]span, ncols)}39+func Layout(s *sheet.Sheet, row, lo, ncols, minCol, maxCol int) []Span {40+ l := rowLayout{s: s, row: row, lo: lo, hi: lo + ncols - 1, out: make([]Span, ncols)}3641 for i := range l.out {37- l.out[i] = span{trail: s.ColWidth(lo + i)}42+ l.out[i] = Span{Trail: s.ColWidth(lo + i)}3843 }3944 l.first, l.last = l.reach(minCol, maxCol)4045 // x[k] is where column first+k starts, relative to column first.
@@ -51,7 +56,7 @@ func rowText(s *sheet.Sheet, row, lo, ncols, minCol, maxCol int) []span {
5156 return l.out5257 }5358 54-// rowLayout is rowText's work in progress on one row.59+// rowLayout is Layout's work in progress on one row.5560 type rowLayout struct {5661 s *sheet.Sheet5762 row int
@@ -59,7 +64,7 @@ type rowLayout struct {
5964 first, last int // the columns whose text may reach them6065 x []int6166 claimed int // text of earlier cells reaches up to here62- out []span67+ out []Span6368 }6469 6570 // content is the cell in column c of the row, or nil if it's blank.
@@ -115,9 +120,9 @@ func (l *rowLayout) place(c int, cell *sheet.Cell) {
115120 if kx1 <= from || kx0 >= to {116121 continue117122 }118- sp := span{trail: kx1 - kx0, style: cell.Style, owner: c}123+ sp := Span{Trail: kx1 - kx0, Style: cell.Style, Owner: c}119124 if seg0, seg1 := max(start, kx0, from), min(start+tw, kx1, to); seg1 > seg0 {120- sp.lead, sp.text, sp.trail = seg0-kx0, cut.cut(seg0-start, seg1-start), kx1-seg1125+ sp.Lead, sp.Text, sp.Trail = seg0-kx0, cut.cut(seg0-start, seg1-start), kx1-seg1121126 }122127 l.out[k-l.lo] = sp123128 }
@@ -168,12 +173,12 @@ func (l *rowLayout) room(c int, cell *sheet.Cell, start, tw int) (from, to int)
168173 // keepGaps keeps a gap where text that runs to the edge of its column169174 // would touch a neighbor that starts at its own edge170175 // ("Groceries9/28/2026").171-func keepGaps(out []span) {176+func keepGaps(out []Span) {172177 for i := 0; i+1 < len(out); i++ {173178 l, r := &out[i], &out[i+1]174- if l.text != "" && l.trail == 0 && r.text != "" && r.lead == 0 && l.owner != r.owner {175- l.text = ansi.Truncate(l.text, ansi.StringWidth(l.text)-1, "")176- l.trail = 1179+ if l.Text != "" && l.Trail == 0 && r.Text != "" && r.Lead == 0 && l.Owner != r.Owner {180+ l.Text = ansi.Truncate(l.Text, ansi.StringWidth(l.Text)-1, "")181+ l.Trail = 1177182 }178183 }179184 }
R internal/ui/view_test.go → internal/ui/rowtext/rowtext_test.go +1 -1
M internal/ui/view.go +13 -12
@@ -8,6 +8,7 @@ import (
88 "github.com/charmbracelet/x/ansi"99 1010 "github.com/FelineStateMachine/012/internal/sheet"11+ "github.com/FelineStateMachine/012/internal/ui/rowtext"1112 "github.com/FelineStateMachine/012/internal/ui/theme"1213 )1314
@@ -134,17 +135,17 @@ func (m *Model) gridRow(row int) string {
134135 135136 _, fc := m.frozen()136137 if fc > 0 {137- b.WriteString(m.cellsText(row, 0, rowText(m.sheet, row, 0, fc, 0, fc-1), focus, sel, selecting))138+ b.WriteString(m.cellsText(row, 0, rowtext.Layout(m.sheet, row, 0, fc, 0, fc-1), focus, sel, selecting))138139 b.WriteString(m.th.FrozenLine.Render("│"))139140 }140141 ncols := m.visibleCols(m.left)141- b.WriteString(m.cellsText(row, m.left, rowText(m.sheet, row, m.left, ncols, fc, sheet.MaxCols-1), focus, sel, selecting))142+ b.WriteString(m.cellsText(row, m.left, rowtext.Layout(m.sheet, row, m.left, ncols, fc, sheet.MaxCols-1), focus, sel, selecting))142143 return b.String()143144 }144145 145146 // cellsText draws the spans of a row's columns from first on, in the146147 // roles for the pointer, the selection, search matches and errors.147-func (m *Model) cellsText(row, first int, spans []span, focus sheet.Addr, sel sheet.Rect, selecting bool) string {148+func (m *Model) cellsText(row, first int, spans []rowtext.Span, focus sheet.Addr, sel sheet.Rect, selecting bool) string {148149 var b strings.Builder149150 for i, sp := range spans {150151 a := sheet.Addr{Col: first + i, Row: row}
@@ -170,7 +171,7 @@ func (m *Model) cellsText(row, first int, spans []span, focus sheet.Addr, sel sh
170171 base, colored = base.Inherit(m.th.Copied), true171172 }172173 if a == m.cur && (m.mode == modeEnter || m.mode == modeEdit) && !m.away() {173- sp = span{text: m.inCellText(m.sheet.ColWidth(a.Col))}174+ sp = rowtext.Span{Text: m.inCellText(m.sheet.ColWidth(a.Col))}174175 } else {175176 m.decorate(&sp, row) // links and error marks, see links.go176177 }
@@ -201,20 +202,20 @@ func (m *Model) dividerRow() string {
201202 202203 // renderSpan draws a span on base, one of the cell roles. Plain cells203204 // with no text style are written without escape codes.204-func renderSpan(th *theme.Theme, sp span, base lipgloss.Style, colored bool) string {205- lead, trail := strings.Repeat(" ", sp.lead), strings.Repeat(" ", sp.trail)206- st := sp.style205+func renderSpan(th *theme.Theme, sp rowtext.Span, base lipgloss.Style, colored bool) string {206+ lead, trail := strings.Repeat(" ", sp.Lead), strings.Repeat(" ", sp.Trail)207+ st := sp.Style207208 st.Align = sheet.AlignAuto208- plain := st.IsZero() && sp.link == "" && !sp.error209+ plain := st.IsZero() && sp.Link == "" && !sp.Error209210 switch {210211 case plain && !colored:211- return lead + sp.text + trail212+ return lead + sp.Text + trail212213 case plain:213- return base.Render(lead + sp.text + trail)214+ return base.Render(lead + sp.Text + trail)214215 case !colored:215- return lead + spanStyle(th, base, sp).Render(sp.text) + trail216+ return lead + spanStyle(th, base, sp).Render(sp.Text) + trail216217 }217- return base.Render(lead) + spanStyle(th, base, sp).Render(sp.text) + base.Render(trail)218+ return base.Render(lead) + spanStyle(th, base, sp).Render(sp.Text) + base.Render(trail)218219 }219220 220221 // inCellText shows the entry being typed inside the cell, keeping the end