Skip to content

Commit

Permalink
Hacked in PlaceOverlay compositor
Browse files Browse the repository at this point in the history
  • Loading branch information
mrusme committed Dec 31, 2022
1 parent 2605daa commit cd7b173
Showing 1 changed file with 185 additions and 0 deletions.
185 changes: 185 additions & 0 deletions ui/helpers/helpers.go
@@ -0,0 +1,185 @@
package helpers

import (
"bytes"
"strings"

"github.com/mattn/go-runewidth"
"github.com/muesli/ansi"
"github.com/muesli/reflow/truncate"
"github.com/muesli/termenv"
)

// Most of this code is borrowed from
// https://github.com/charmbracelet/lipgloss/pull/102
// as well as the lipgloss library.

// Split a string into lines, additionally returning the size of the widest
// line.
func getLines(s string) (lines []string, widest int) {
lines = strings.Split(s, "\n")

for _, l := range lines {
w := ansi.PrintableRuneWidth(l)
if widest < w {
widest = w
}
}

return lines, widest
}

// PlaceOverlay places fg on top of bg.
func PlaceOverlay(x, y int, fg, bg string, opts ...WhitespaceOption) string {
fgLines, fgWidth := getLines(fg)
bgLines, bgWidth := getLines(bg)
bgHeight := len(bgLines)
fgHeight := len(fgLines)

if fgWidth >= bgWidth && fgHeight >= bgHeight {
// FIXME: return fg or bg?
return fg
}
// TODO: allow placement outside of the bg box?
x = clamp(x, 0, bgWidth-fgWidth)
y = clamp(y, 0, bgHeight-fgHeight)

ws := &whitespace{}
for _, opt := range opts {
opt(ws)
}

var b strings.Builder
for i, bgLine := range bgLines {
if i > 0 {
b.WriteByte('\n')
}
if i < y || i >= y+fgHeight {
b.WriteString(bgLine)
continue
}

pos := 0
if x > 0 {
left := truncate.String(bgLine, uint(x))
pos = ansi.PrintableRuneWidth(left)
b.WriteString(left)
if pos < x {
b.WriteString(ws.render(x - pos))
pos = x
}
}

fgLine := fgLines[i-y]
b.WriteString(fgLine)
pos += ansi.PrintableRuneWidth(fgLine)

right := cutLeft(bgLine, pos)
bgWidth := ansi.PrintableRuneWidth(bgLine)
rightWidth := ansi.PrintableRuneWidth(right)
if rightWidth <= bgWidth-pos {
b.WriteString(ws.render(bgWidth - rightWidth - pos))
}

b.WriteString(right)
}

return b.String()
}

// cutLeft cuts printable characters from the left.
// This function is heavily based on muesli's ansi and truncate packages.
func cutLeft(s string, cutWidth int) string {
var (
pos int
isAnsi bool
ab bytes.Buffer
b bytes.Buffer
)
for _, c := range s {
var w int
if c == ansi.Marker || isAnsi {
isAnsi = true
ab.WriteRune(c)
if ansi.IsTerminator(c) {
isAnsi = false
if bytes.HasSuffix(ab.Bytes(), []byte("[0m")) {
ab.Reset()
}
}
} else {
w = runewidth.RuneWidth(c)
}

if pos >= cutWidth {
if b.Len() == 0 {
if ab.Len() > 0 {
b.Write(ab.Bytes())
}
if pos-cutWidth > 1 {
b.WriteByte(' ')
continue
}
}
b.WriteRune(c)
}
pos += w
}
return b.String()
}

func clamp(v, lower, upper int) int {
return min(max(v, lower), upper)
}

func max(a, b int) int {
if a > b {
return a
}
return b
}

func min(a, b int) int {
if a < b {
return a
}
return b
}

type whitespace struct {
style termenv.Style
chars string
}

// Render whitespaces.
func (w whitespace) render(width int) string {
if w.chars == "" {
w.chars = " "
}

r := []rune(w.chars)
j := 0
b := strings.Builder{}

// Cycle through runes and print them into the whitespace.
for i := 0; i < width; {
b.WriteRune(r[j])
j++
if j >= len(r) {
j = 0
}
i += ansi.PrintableRuneWidth(string(r[j]))
}

// Fill any extra gaps white spaces. This might be necessary if any runes
// are more than one cell wide, which could leave a one-rune gap.
short := width - ansi.PrintableRuneWidth(b.String())
if short > 0 {
b.WriteString(strings.Repeat(" ", short))
}

return w.style.Styled(b.String())
}

// WhitespaceOption sets a styling rule for rendering whitespace.
type WhitespaceOption func(*whitespace)

0 comments on commit cd7b173

Please sign in to comment.