From a075bfc9317152e674d661a2cdfe58144306e77a Mon Sep 17 00:00:00 2001 From: Bastian Winkler Date: Sun, 21 Aug 2022 18:53:48 +0200 Subject: [PATCH] feat(position): Add function to place an overlay --- position.go | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/position.go b/position.go index 2ecb8979..89da10fe 100644 --- a/position.go +++ b/position.go @@ -1,10 +1,13 @@ package lipgloss import ( + "bytes" "math" "strings" + "github.com/mattn/go-runewidth" "github.com/muesli/reflow/ansi" + "github.com/muesli/reflow/truncate" ) // Position represents a position along a horizontal or vertical axis. It's in @@ -138,3 +141,106 @@ func PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOptio return b.String() } + +// 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) +}