| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- package layout
- import (
- "strings"
- "github.com/charmbracelet/lipgloss"
- chAnsi "github.com/charmbracelet/x/ansi"
- "github.com/muesli/ansi"
- "github.com/muesli/reflow/truncate"
- "github.com/muesli/termenv"
- "github.com/sst/opencode/internal/tui/styles"
- "github.com/sst/opencode/internal/tui/theme"
- "github.com/sst/opencode/internal/tui/util"
- )
- // Most of this code is borrowed from
- // https://github.com/charmbracelet/lipgloss/pull/102
- // as well as the lipgloss library, with some modification for what I needed.
- // 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,
- shadow bool, opts ...WhitespaceOption,
- ) string {
- fgLines, fgWidth := getLines(fg)
- bgLines, bgWidth := getLines(bg)
- bgHeight := len(bgLines)
- fgHeight := len(fgLines)
- if shadow {
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
- var shadowbg string = ""
- shadowchar := lipgloss.NewStyle().
- Background(t.BackgroundDarker()).
- Foreground(t.Background()).
- Render("░")
- bgchar := baseStyle.Render(" ")
- for i := 0; i <= fgHeight; i++ {
- if i == 0 {
- shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n"
- } else {
- shadowbg += bgchar + strings.Repeat(shadowchar, fgWidth) + "\n"
- }
- }
- fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...)
- fgLines, fgWidth = getLines(fg)
- fgHeight = len(fgLines)
- }
- if fgWidth >= bgWidth && fgHeight >= bgHeight {
- // FIXME: return fg or bg?
- return fg
- }
- // TODO: allow placement outside of the bg box?
- x = util.Clamp(x, 0, bgWidth-fgWidth)
- y = util.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 {
- return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
- }
- func max(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)
|