borders.go 10 KB

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