borders.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. package lipgloss
  2. import (
  3. "strings"
  4. "github.com/mattn/go-runewidth"
  5. "github.com/muesli/reflow/ansi"
  6. "github.com/muesli/termenv"
  7. )
  8. // Border contains a series of values which comprise the various parts of a
  9. // border.
  10. type Border struct {
  11. Top string
  12. Bottom string
  13. Left string
  14. Right string
  15. TopLeft string
  16. TopRight string
  17. BottomRight string
  18. BottomLeft string
  19. }
  20. // GetTopSize returns the width of the top border. If borders contain runes of
  21. // varying widths, the widest rune is returned. If no border exists on the top
  22. // edge, 0 is returned.
  23. func (b Border) GetTopSize() int {
  24. return getBorderEdgeWidth(b.TopLeft, b.Top, b.TopRight)
  25. }
  26. // GetRightSize returns the width of the right border. If borders contain
  27. // runes of varying widths, the widest rune is returned. If no border exists on
  28. // the right edge, 0 is returned.
  29. func (b Border) GetRightSize() int {
  30. return getBorderEdgeWidth(b.TopRight, b.Top, b.BottomRight)
  31. }
  32. // GetBottomSize returns the width of the bottom border. If borders contain
  33. // runes of varying widths, the widest rune is returned. If no border exists on
  34. // the bottom edge, 0 is returned.
  35. func (b Border) GetBottomSize() int {
  36. return getBorderEdgeWidth(b.BottomLeft, b.Bottom, b.BottomRight)
  37. }
  38. // GetLeftSize returns the width of the left border. If borders contain runes
  39. // of varying widths, the widest rune is returned. If no border exists on the
  40. // left edge, 0 is returned.
  41. func (b Border) GetLeftSize() int {
  42. return getBorderEdgeWidth(b.TopLeft, b.Left, b.TopRight)
  43. }
  44. func getBorderEdgeWidth(borderParts ...string) (maxWidth int) {
  45. for _, piece := range borderParts {
  46. w := maxRuneWidth(piece)
  47. if w > maxWidth {
  48. maxWidth = w
  49. }
  50. }
  51. return maxWidth
  52. }
  53. var (
  54. noBorder = Border{}
  55. normalBorder = Border{
  56. Top: "─",
  57. Bottom: "─",
  58. Left: "│",
  59. Right: "│",
  60. TopLeft: "┌",
  61. TopRight: "┐",
  62. BottomLeft: "└",
  63. BottomRight: "┘",
  64. }
  65. roundedBorder = Border{
  66. Top: "─",
  67. Bottom: "─",
  68. Left: "│",
  69. Right: "│",
  70. TopLeft: "╭",
  71. TopRight: "╮",
  72. BottomLeft: "╰",
  73. BottomRight: "╯",
  74. }
  75. blockBorder = Border{
  76. Top: "█",
  77. Bottom: "█",
  78. Left: "█",
  79. Right: "█",
  80. TopLeft: "█",
  81. TopRight: "█",
  82. BottomLeft: "█",
  83. BottomRight: "█",
  84. }
  85. outerHalfBlockBorder = Border{
  86. Top: "▀",
  87. Bottom: "▄",
  88. Left: "▌",
  89. Right: "▐",
  90. TopLeft: "▛",
  91. TopRight: "▜",
  92. BottomLeft: "▙",
  93. BottomRight: "▟",
  94. }
  95. innerHalfBlockBorder = Border{
  96. Top: "▄",
  97. Bottom: "▀",
  98. Left: "▐",
  99. Right: "▌",
  100. TopLeft: "▗",
  101. TopRight: "▖",
  102. BottomLeft: "▝",
  103. BottomRight: "▘",
  104. }
  105. thickBorder = Border{
  106. Top: "━",
  107. Bottom: "━",
  108. Left: "┃",
  109. Right: "┃",
  110. TopLeft: "┏",
  111. TopRight: "┓",
  112. BottomLeft: "┗",
  113. BottomRight: "┛",
  114. }
  115. doubleBorder = Border{
  116. Top: "═",
  117. Bottom: "═",
  118. Left: "║",
  119. Right: "║",
  120. TopLeft: "╔",
  121. TopRight: "╗",
  122. BottomLeft: "╚",
  123. BottomRight: "╝",
  124. }
  125. hiddenBorder = Border{
  126. Top: " ",
  127. Bottom: " ",
  128. Left: " ",
  129. Right: " ",
  130. TopLeft: " ",
  131. TopRight: " ",
  132. BottomLeft: " ",
  133. BottomRight: " ",
  134. }
  135. )
  136. // NormalBorder returns a standard-type border with a normal weight and 90
  137. // degree corners.
  138. func NormalBorder() Border {
  139. return normalBorder
  140. }
  141. // RoundedBorder returns a border with rounded corners.
  142. func RoundedBorder() Border {
  143. return roundedBorder
  144. }
  145. // BlockBorder returns a border that takes the whole block.
  146. func BlockBorder() Border {
  147. return blockBorder
  148. }
  149. // OuterHalfBlockBorder returns a half-block border that sits outside the frame.
  150. func OuterHalfBlockBorder() Border {
  151. return outerHalfBlockBorder
  152. }
  153. // InnerHalfBlockBorder returns a half-block border that sits inside the frame.
  154. func InnerHalfBlockBorder() Border {
  155. return innerHalfBlockBorder
  156. }
  157. // ThickBorder returns a border that's thicker than the one returned by
  158. // NormalBorder.
  159. func ThickBorder() Border {
  160. return thickBorder
  161. }
  162. // DoubleBorder returns a border comprised of two thin strokes.
  163. func DoubleBorder() Border {
  164. return doubleBorder
  165. }
  166. // HiddenBorder returns a border that renders as a series of single-cell
  167. // spaces. It's useful for cases when you want to remove a standard border but
  168. // maintain layout positioning. This said, you can still apply a background
  169. // color to a hidden border.
  170. func HiddenBorder() Border {
  171. return hiddenBorder
  172. }
  173. func (s Style) applyBorder(str string) string {
  174. var (
  175. topSet = s.isSet(borderTopKey)
  176. rightSet = s.isSet(borderRightKey)
  177. bottomSet = s.isSet(borderBottomKey)
  178. leftSet = s.isSet(borderLeftKey)
  179. border = s.getBorderStyle()
  180. hasTop = s.getAsBool(borderTopKey, false)
  181. hasRight = s.getAsBool(borderRightKey, false)
  182. hasBottom = s.getAsBool(borderBottomKey, false)
  183. hasLeft = s.getAsBool(borderLeftKey, false)
  184. topFG = s.getAsColor(borderTopForegroundKey)
  185. rightFG = s.getAsColor(borderRightForegroundKey)
  186. bottomFG = s.getAsColor(borderBottomForegroundKey)
  187. leftFG = s.getAsColor(borderLeftForegroundKey)
  188. topBG = s.getAsColor(borderTopBackgroundKey)
  189. rightBG = s.getAsColor(borderRightBackgroundKey)
  190. bottomBG = s.getAsColor(borderBottomBackgroundKey)
  191. leftBG = s.getAsColor(borderLeftBackgroundKey)
  192. )
  193. // If a border is set and no sides have been specifically turned on or off
  194. // render borders on all sides.
  195. if border != noBorder && !(topSet || rightSet || bottomSet || leftSet) {
  196. hasTop = true
  197. hasRight = true
  198. hasBottom = true
  199. hasLeft = true
  200. }
  201. // If no border is set or all borders are been disabled, abort.
  202. if border == noBorder || (!hasTop && !hasRight && !hasBottom && !hasLeft) {
  203. return str
  204. }
  205. lines, width := getLines(str)
  206. if hasLeft {
  207. if border.Left == "" {
  208. border.Left = " "
  209. }
  210. width += maxRuneWidth(border.Left)
  211. }
  212. if hasRight && border.Right == "" {
  213. border.Right = " "
  214. }
  215. // If corners should be rendered but are set with the empty string, fill them
  216. // with a single space.
  217. if hasTop && hasLeft && border.TopLeft == "" {
  218. border.TopLeft = " "
  219. }
  220. if hasTop && hasRight && border.TopRight == "" {
  221. border.TopRight = " "
  222. }
  223. if hasBottom && hasLeft && border.BottomLeft == "" {
  224. border.BottomLeft = " "
  225. }
  226. if hasBottom && hasRight && border.BottomRight == "" {
  227. border.BottomRight = " "
  228. }
  229. // Figure out which corners we should actually be using based on which
  230. // sides are set to show.
  231. if hasTop {
  232. switch {
  233. case !hasLeft && !hasRight:
  234. border.TopLeft = ""
  235. border.TopRight = ""
  236. case !hasLeft:
  237. border.TopLeft = ""
  238. case !hasRight:
  239. border.TopRight = ""
  240. }
  241. }
  242. if hasBottom {
  243. switch {
  244. case !hasLeft && !hasRight:
  245. border.BottomLeft = ""
  246. border.BottomRight = ""
  247. case !hasLeft:
  248. border.BottomLeft = ""
  249. case !hasRight:
  250. border.BottomRight = ""
  251. }
  252. }
  253. // For now, limit corners to one rune.
  254. border.TopLeft = getFirstRuneAsString(border.TopLeft)
  255. border.TopRight = getFirstRuneAsString(border.TopRight)
  256. border.BottomRight = getFirstRuneAsString(border.BottomRight)
  257. border.BottomLeft = getFirstRuneAsString(border.BottomLeft)
  258. var out strings.Builder
  259. // Render top
  260. if hasTop {
  261. top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
  262. top = s.styleBorder(top, topFG, topBG)
  263. out.WriteString(top)
  264. out.WriteRune('\n')
  265. }
  266. leftRunes := []rune(border.Left)
  267. leftIndex := 0
  268. rightRunes := []rune(border.Right)
  269. rightIndex := 0
  270. // Render sides
  271. for i, l := range lines {
  272. if hasLeft {
  273. r := string(leftRunes[leftIndex])
  274. leftIndex++
  275. if leftIndex >= len(leftRunes) {
  276. leftIndex = 0
  277. }
  278. out.WriteString(s.styleBorder(r, leftFG, leftBG))
  279. }
  280. out.WriteString(l)
  281. if hasRight {
  282. r := string(rightRunes[rightIndex])
  283. rightIndex++
  284. if rightIndex >= len(rightRunes) {
  285. rightIndex = 0
  286. }
  287. out.WriteString(s.styleBorder(r, rightFG, rightBG))
  288. }
  289. if i < len(lines)-1 {
  290. out.WriteRune('\n')
  291. }
  292. }
  293. // Render bottom
  294. if hasBottom {
  295. bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width)
  296. bottom = s.styleBorder(bottom, bottomFG, bottomBG)
  297. out.WriteRune('\n')
  298. out.WriteString(bottom)
  299. }
  300. return out.String()
  301. }
  302. // Render the horizontal (top or bottom) portion of a border.
  303. func renderHorizontalEdge(left, middle, right string, width int) string {
  304. if width < 1 {
  305. return ""
  306. }
  307. if middle == "" {
  308. middle = " "
  309. }
  310. leftWidth := ansi.PrintableRuneWidth(left)
  311. rightWidth := ansi.PrintableRuneWidth(right)
  312. runes := []rune(middle)
  313. j := 0
  314. out := strings.Builder{}
  315. out.WriteString(left)
  316. for i := leftWidth + rightWidth; i < width+rightWidth; {
  317. out.WriteRune(runes[j])
  318. j++
  319. if j >= len(runes) {
  320. j = 0
  321. }
  322. i += ansi.PrintableRuneWidth(string(runes[j]))
  323. }
  324. out.WriteString(right)
  325. return out.String()
  326. }
  327. // Apply foreground and background styling to a border.
  328. func (s Style) styleBorder(border string, fg, bg TerminalColor) string {
  329. if fg == noColor && bg == noColor {
  330. return border
  331. }
  332. var style = termenv.Style{}
  333. if fg != noColor {
  334. style = style.Foreground(fg.color(s.r))
  335. }
  336. if bg != noColor {
  337. style = style.Background(bg.color(s.r))
  338. }
  339. return style.Styled(border)
  340. }
  341. func maxRuneWidth(str string) (width int) {
  342. for _, r := range str {
  343. w := runewidth.RuneWidth(r)
  344. if w > width {
  345. width = w
  346. }
  347. }
  348. return width
  349. }
  350. func getFirstRuneAsString(str string) string {
  351. if str == "" {
  352. return str
  353. }
  354. r := []rune(str)
  355. return string(r[0])
  356. }