charmbracelet / lipgloss

Style definitions for nice terminal layouts 👄
MIT License
7.89k stars 225 forks source link

Background style is not rendered when wrapping other rendered styles #209

Open drakenstar opened 1 year ago

drakenstar commented 1 year ago

Describe the bug When rendering strings with backgrounds already applied, the background from our rendering style has issues. In this test case, I've joined 3 other strings using JoinHorizontal that each have their own background. Note the missing background beneath the "right text" section:

Note the area beneath "right text" that does not have a background style applied.

Screen Shot 2023-07-26 at 12 07 59 am

Setup

Source Code Simple reproduce:

    redStyle := lipgloss.NewStyle().
        Background(lipgloss.Color("#FF0000")).
        Width(10)

    greenStyle := lipgloss.NewStyle().
        Background(lipgloss.Color("#00FF00")).
        Width(10)

    outerStyle := lipgloss.NewStyle().
        Width(40).
        Background(lipgloss.Color("#0000FF"))

    fmt.Print(outerStyle.Render(lipgloss.JoinHorizontal(0,
        greenStyle.Render("left text"),
        redStyle.Render("multi\nline\ncenter\ntext"),
        greenStyle.Render("right text"),
    )))

Expected behavior In this case I would have expected the blue background from style3 to be rendered in all spaces where there is no background applied.

drakenstar commented 1 year ago

Ok I dug into this and I think found the reason for this output. wordwrap.String called by Style.Render will not output spaces at the end of a line unless it finds a \n character before the limit. For example:

s := wordwrap.String("test  test  ", 6)
fmt.Print(s)
// "test\ntest"

s := wordwrap.String("test  \ntest  ", 6)
fmt.Print(s)
// "test  \ntest"

This is arguably surprising from their end, however lipgloss could workaround this by trimming whitespace this output if it is not styled by seeking backwards per line for \x1b[0m and trimming spaces after it.

I'm happy to PR this if there's agreement it should be fixed.

meowgorithm commented 1 year ago

Okay yep, this is indeed a bug. I suspect the solution may be may complex than this, though we'll need to look into it further before we can have an opinion on the fix.

Here's a centered use case to further illustrate the issue.

image
package main

import (
    "fmt"

    "github.com/charmbracelet/lipgloss"
)

func main() {
    redStyle := lipgloss.NewStyle().
        Background(lipgloss.Color("#FF0000")).
        Width(10)

    greenStyle := lipgloss.NewStyle().
        Background(lipgloss.Color("#00FF00")).
        Width(10)

    outerStyle := lipgloss.NewStyle().
        Width(40).
        Background(lipgloss.Color("#0000FF"))

    fmt.Println(outerStyle.Render(lipgloss.JoinHorizontal(lipgloss.Center,
        greenStyle.Render("left text"),
        redStyle.Render("multi\nline\ncenter\ntext\nwow"),
        greenStyle.Render("right text"),
    )))
}
drakenstar commented 1 year ago

I've worked around this locally doing as I described above and trimming space characters from the end of a line if they are immediately preceded by a reset sequence.

However there's another harder case that workaround still doesn't solve:

redStyle := lipgloss.NewStyle().
        Background(lipgloss.Color("#FF0000")).
        Width(10)

    greenStyle := lipgloss.NewStyle().
        Background(lipgloss.Color("#00FF00")).
        Width(10)

    outerStyle := lipgloss.NewStyle().
        Width(40).
        Background(lipgloss.Color("#0000FF"))

    fmt.Println(outerStyle.Render(lipgloss.JoinHorizontal(0,
        redStyle.Render("multi\nline"),
        greenStyle.Render("left text"),
        redStyle.Render("multi\nline"),
    )))

Resulting in:

image

A more complete solve for this is probably inspecting each line for segments that have no styling any applying the current style to them.

meowgorithm commented 1 year ago

For now, I'd probably workaround this by simply placing the short column (or all columns) over the appropriate background color with lipgloss.Place (see example and docs).

Here’s how I'd fix my above example (in a real scenario I'd abstract the Place stuff and generalize it to work for all columns):

package main

import (
    "fmt"

    "github.com/charmbracelet/lipgloss"
)

func main() {
    const blue = lipgloss.Color("#0000FF")

    redStyle := lipgloss.NewStyle().
        Background(lipgloss.Color("#FF0000")).
        Width(10)

    greenStyle := lipgloss.NewStyle().
        Background(lipgloss.Color("#00FF00")).
        Width(10)

    outerStyle := lipgloss.NewStyle().
        Width(40).
        Background(blue)

    leftContent := greenStyle.Render("left text")
    middleContent := redStyle.Render("multi\nline\ncenter\ntext\nwow")
    rightContent := greenStyle.Render("right text")

    fmt.Println(outerStyle.Render(
        lipgloss.JoinHorizontal(
            lipgloss.Center,
            leftContent,
            middleContent,
            lipgloss.Place(
                lipgloss.Width(rightContent),            // width
                lipgloss.Height(middleContent),          // height
                lipgloss.Left,                           // x
                lipgloss.Center,                         // y
                rightContent,                            // content
                lipgloss.WithWhitespaceBackground(blue), // background
            ),
        )))
}

Output:

image