cat / 012
clone

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

main
012 / internal / ui / looks.go
looks.go 4.8K
1package ui23import (4 "image/color"5 "strings"67 "charm.land/lipgloss/v2"8 "github.com/charmbracelet/x/ansi"910 "src.rest/cat/012/internal/sheet"11 "src.rest/cat/012/internal/ui/rowtext"12 "src.rest/cat/012/internal/ui/theme"13)1415// Drawing the sheet's rules (see sheet.Look): a conditional format's16// fill, text color and text styles, a color scale's shade, checkboxes,17// the ▾ of a dropdown and the mark of an invalid entry. Only the visible18// cells are asked for their looks, and the sheet keeps them until the19// next recalculation, so a rule over thousands of rows costs a frame20// what the screen shows.2122// checkGlyphs are a checkbox's text, unchecked and checked, drawn23// centered like a boolean. ASCII brackets keep one width in every24// terminal, where ballot box glyphs don't.25var checkGlyphs = [2]string{"[ ]", "[✓]"}2627// ruleSpan applies the looks of cell a and of the owner of its text to28// a span: its text styles and a checkbox's glyph, and, where the cell29// isn't colored already, the shade it's drawn on, if any (shaded).30func (m *Model) ruleSpan(a sheet.Addr, sp *rowtext.Span, colored bool, rgb func(int) color.Color) (look sheet.Look, shade theme.Shade, shaded bool) {31 look = m.sheet.Look(a)32 own := look33 if sp.Owner != a.Col && sp.Text != "" {34 own = m.sheet.Look(sheet.Addr{Col: sp.Owner, Row: a.Row})35 }36 if look.Checkbox {37 *sp = checkSpan(look.Checked, m.sheet.ColWidth(a.Col), m.sheet.CellStyle(a))38 own = look39 }40 if own.Styled {41 st := own.Style42 sp.Style.Bold = sp.Style.Bold || st.Bold43 sp.Style.Italic = sp.Style.Italic || st.Italic44 sp.Style.Underline = sp.Style.Underline || st.Underline45 sp.Style.Strikethrough = sp.Style.Strikethrough || st.Strikethrough46 }47 sp.Invalid = own.Invalid && sp.Text != ""48 switch {49 case colored:50 case look.Scaled:51 return look, m.th.ScaleFill(look.From, look.To, look.Pos, rgb), true52 case look.Styled || own.Styled:53 st := sheet.RuleStyle{}54 if look.Styled {55 st.Fill = look.Style.Fill56 }57 if own.Styled {58 st.Text = own.Style.Text59 }60 if st.Fill != sheet.ColorNone || st.Text != sheet.ColorNone {61 return look, m.th.RuleShade(st), true62 }63 }64 return look, theme.Shade{}, false65}6667// checkSpan is a checkbox drawn in a column w wide.68func checkSpan(on bool, w int, st sheet.Style) rowtext.Span {69 g := checkGlyphs[0]70 if on {71 g = checkGlyphs[1]72 }73 if w < 3 {74 g = ansi.Truncate(g[1:], w, "")75 }76 gw := ansi.StringWidth(g)77 lead := (w - gw) / 278 st.Underline = false // an underline would run under the brackets79 return rowtext.Span{Lead: lead, Text: g, Trail: w - gw - lead, Style: st}80}8182// dropdownMark puts the ▾ of a dropdown cell in its last column.83func (m *Model) dropdownMark(text string, w int, base lipgloss.Style, colored bool) string {84 if w < 3 {85 return text86 }87 mark := m.th.Dropdown.Render("▾")88 if colored {89 mark = base.Render("▾")90 }91 return ansi.Truncate(text, w-1, "") + mark92}9394// slotColor is the terminal's color for an ANSI slot, as color scales95// blend it.96func (m *Model) slotColor(i int) color.Color { return m.term.color(i) }9798// checkboxAt reports where the glyph of the checkbox at a is on screen,99// as columns from..to of the row, or false if a isn't a checkbox.100func (m *Model) checkboxAt(a sheet.Addr, start int) (from, to int, ok bool) {101 if !m.sheet.HasRules() || !m.sheet.Look(a).Checkbox {102 return 0, 0, false103 }104 sp := checkSpan(false, m.sheet.ColWidth(a.Col), sheet.Style{})105 return start + sp.Lead, start + sp.Lead + ansi.StringWidth(sp.Text) - 1, true106}107108// ruleHit is what a click at x on the cell at a starting at start hits109// among its rules: a checkbox's glyph or a dropdown's ▾.110func (m *Model) ruleHit(a sheet.Addr, x, start int) hitKind {111 if m.mode != modeReady || !m.sheet.HasRules() {112 return hitCell113 }114 if from, to, ok := m.checkboxAt(a, start); ok && x >= from && x <= to {115 return hitCheckbox116 }117 if w := m.sheet.ColWidth(a.Col); w >= 3 && x == start+w-1 && m.sheet.Look(a).Dropdown {118 return hitDropdown119 }120 return hitCell121}122123// validationLine is the context line for the active cell's validation:124// why its entry is invalid, the rule's help text, or the keys of a125// checkbox or dropdown.126func (m *Model) validationLine() string {127 if !m.sheet.HasRules() {128 return ""129 }130 v, ok := m.sheet.Validation(m.cur)131 if !ok {132 return ""133 }134 look := m.sheet.Look(m.cur)135 var keys string136 switch {137 case look.Checkbox:138 keys = m.th.KeyHints("Space", "check or uncheck")139 case look.Dropdown:140 keys = m.th.KeyHints(m.shortcut("data.dropdown"), "choose from the list")141 }142 var text string143 switch {144 case look.Invalid:145 text = m.th.Warning.Render("Invalid: " + v.HelpText())146 case strings.TrimSpace(v.Help) != "":147 text = m.th.Muted.Render(v.Help)148 }149 switch {150 case text == "":151 return keys152 case keys == "" || ansi.StringWidth(text)+3+ansi.StringWidth(keys) > m.width:153 return text154 }155 return text + " " + keys156}