1package ui23import (4 "strings"56 "charm.land/lipgloss/v2"7 "github.com/charmbracelet/x/ansi"89 "src.rest/cat/012/internal/sheet"10 "src.rest/cat/012/internal/ui/rowtext"11 "src.rest/cat/012/internal/ui/theme"12)1314// Drawing data bars, icon sets and dropdown chips (see sheet.Look), in15// the terminal's own glyphs so each reads without color: a bar is16// eighth blocks across the blank columns it covers and reverse video17// under the text it runs beneath; an icon is a glyph at the cell's18// left; a chip is the value in reverse video between rounded ends.1920// cellColumns splits a span into what each of its w columns shows: a21// space, or a grapheme with "" after it for each further column it's22// wide.23func cellColumns(sp rowtext.Span, w int) []string {24 cols := make([]string, 0, w)25 for range sp.Lead {26 cols = append(cols, " ")27 }28 for s := sp.Text; s != "" && len(cols) < w; {29 g, gw := ansi.FirstGraphemeCluster(s, ansi.GraphemeWidth)30 s = s[len(g):]31 if gw == 0 {32 continue33 }34 cols = append(cols, g)35 for range gw - 1 {36 cols = append(cols, "")37 }38 }39 for len(cols) < w {40 cols = append(cols, " ")41 }42 return cols[:w]43}4445// iconColumn is where an icon goes in a column w wide.46func iconColumn(w int) int { return min(1, w-1) }4748// withIcon makes room for an icon at the left of a cell's text: text49// that would start within two columns of it moves right, cut at the50// cell's edge; a value shown without its icon's value is dropped.51func withIcon(sp rowtext.Span, w int, hidden bool) rowtext.Span {52 if hidden {53 return rowtext.Span{Trail: w, Owner: sp.Owner}54 }55 start := iconColumn(w) + 256 if sp.Text == "" || sp.Lead >= start {57 return sp58 }59 sp.Text = ansi.Truncate(sp.Text, max(w-start, 0), "")60 sp.Lead = start61 sp.Trail = max(w-start-ansi.StringWidth(sp.Text), 0)62 return sp63}6465// barText draws the cell at a, of w columns, with its data bar or icon:66// sp is its text on this line, drawn on base (colored when that's a67// highlight, which the bar's and icon's colors give way to), and value68// whether this is the line its value is on.69func (m *Model) barText(sp rowtext.Span, w int, look *sheet.Look, base *lipgloss.Style, colored, value bool) string {70 icon := value && look.Icon != ""71 if icon || look.ValueHidden {72 sp = withIcon(sp, w, look.ValueHidden)73 }74 cols := cellColumns(sp, w)75 var cover []int76 if look.Bar {77 cover = theme.BarCover(look.BarLen, w-1) // the last column keeps bars apart78 }79 text := base80 if !plainSpan(sp) {81 st := spanStyle(&m.th, *base, sp)82 text = &st83 }84 // A run is drawn in the cell's role (penBase), its text's style85 // (penText) or a role of the theme (penRole, role). The theme's roles86 // are held apart from base, so base, often a shade on the caller's87 // stack, never flows into the frame's cache of shades.88 var b strings.Builder89 var run strings.Builder90 cur, curRole := penBase, (*lipgloss.Style)(nil)91 flush := func() {92 switch {93 case run.Len() == 0:94 case cur == penBase && !colored:95 b.WriteString(run.String())96 case cur == penBase:97 b.WriteString(base.Render(run.String()))98 case cur == penText:99 b.WriteString(text.Render(run.String()))100 default: // a theme's role, whose address stays put for the frame101 b.WriteString(m.shade(curRole).Wrap(run.String()))102 }103 run.Reset()104 }105 put := func(p pen, role *lipgloss.Style, s string) {106 if p == penText && text == base {107 p = penBase108 }109 if p != cur || role != curRole {110 flush()111 cur, curRole = p, role112 }113 run.WriteString(s)114 }115 for x, ch := range cols {116 switch n := coverAt(cover, x); {117 case ch == "":118 case icon && x == iconColumn(w):119 p, role := m.ruleRole(look.IconColor, colored)120 put(p, role, look.Icon)121 case n > 0 && ch == " ":122 p, role := m.ruleRole(look.BarColor, colored)123 put(p, role, theme.Block(n))124 case n >= 4 && !colored:125 put(penRole, &m.th.BarOn[look.BarColor], ch)126 case ch == " ":127 put(penBase, nil, ch)128 default:129 put(penText, nil, ch)130 }131 }132 flush()133 return b.String()134}135136// shade is role with its escape codes, kept for the frame.137func (m *Model) shade(role *lipgloss.Style) theme.Shade {138 if sh, ok := m.shaded[role]; ok {139 return sh140 }141 if m.shaded == nil {142 m.shaded = map[*lipgloss.Style]theme.Shade{}143 }144 sh := theme.ShadeOf(*role)145 m.shaded[role] = sh146 return sh147}148149func coverAt(cover []int, x int) int {150 if x < len(cover) {151 return cover[x]152 }153 return 0154}155156// pen is what barText draws a run of a cell's columns in.157type pen uint8158159const (160 penBase pen = iota // the cell's role161 penText // its text's style162 penRole // a role of the theme, kept for the frame163)164165// ruleRole is the pen a rule color draws a glyph in: the color's role,166// or the cell's own role on a highlighted cell.167func (m *Model) ruleRole(c sheet.Color, colored bool) (pen, *lipgloss.Style) {168 if colored || c == sheet.ColorNone || int(c) >= sheet.NumColors {169 return penBase, nil170 }171 return penRole, &m.th.RuleText[c]172}173174// chipText draws a dropdown's value as a chip in a column w wide: its175// rounded ends around the value and ▾, the value cut to fit; false when176// the cell shows no value to put on one.177func (m *Model) chipText(sp rowtext.Span, w int) (string, bool) {178 value := strings.TrimSpace(sp.Text)179 room := w - 5 // padding, two ends, a space and the ▾180 if value == "" || room < 1 {181 return "", false182 }183 value = ansi.Truncate(value, room, "…")184 chip := m.paint(&m.th.DropdownCap, "▐") + m.th.DropdownChip.Render(value+" ▾") + m.paint(&m.th.DropdownCap, "▌")185 return " " + chip + strings.Repeat(" ", w-4-ansi.StringWidth(value)-1), true186}