Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lipgloss styles rendered incorrectly when .Width() applied #1225

Open
ipsavitsky opened this issue Nov 6, 2024 · 12 comments · Fixed by #1227
Open

lipgloss styles rendered incorrectly when .Width() applied #1225

ipsavitsky opened this issue Nov 6, 2024 · 12 comments · Fixed by #1227

Comments

@ipsavitsky
Copy link

Describe the bug
When applying a style dynamically to a string that has a width set to it by a lipgloss style, the style is rendered incorrectly. With 3 different styles applied to 3 different substrings there should be "typed" text (default), "untyped text" (default but faint) and cursor (default reversed), when applied to slices of a string, render incorrectly. The beginning of the string (which should be typed) is rendered fainted. Also, the borders and some text outside of the widget appear to be rendered fainted. This effect is temporal, as on first rendered frame everything appears to look fine and then rerenders incorrectly on subsequent renders (see video).

Setup
Please complete the following information along with version numbers, if applicable.

  • OS Ubuntu
  • Shell bash
  • Terminal Emulator kitty
  • Terminal Multiplexer zellij

To Reproduce
Steps to reproduce the behavior:

  1. Compile the code
  2. Start the program
  3. See error

Source Code

package main

import (
	"fmt"
	"strings"
	"time"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"golang.org/x/term"

	"github.com/charmbracelet/bubbles/stopwatch"
)

func main() {
	prompt := strings.Repeat("the end is never ", 100)

	p := tea.NewProgram(initialModel(prompt), tea.WithAltScreen())
	p.Run()
}

type styles struct {
	global_style       lipgloss.Style
	typed_text_style   lipgloss.Style
	untyped_text_style lipgloss.Style
	cursor_style       lipgloss.Style
}

type Model struct {
	prompt    string
	cursor    int
	styles    styles
	width     int
	stopwatch stopwatch.Model
}

func default_styles(width int) styles {
	return styles{
		global_style:       lipgloss.NewStyle().Width(width).Border(lipgloss.NormalBorder()),
		typed_text_style:   lipgloss.NewStyle(),
		untyped_text_style: lipgloss.NewStyle().Faint(true),
		cursor_style:       lipgloss.NewStyle().Reverse(true),
	}
}

func initialModel(prompt string) Model {
	w, _, _ := term.GetSize(0)
	return Model{
		width:     w,
		prompt:    prompt,
		cursor:    0,
		styles:    default_styles(w - 10),
		stopwatch: stopwatch.NewWithInterval(time.Second),
	}
}

func (m Model) Init() tea.Cmd {
	return m.stopwatch.Start()
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmds []tea.Cmd
	var cmd tea.Cmd
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.Type {
		case tea.KeyCtrlC, tea.KeyEsc:
			return m, tea.Quit
		}
	case stopwatch.TickMsg:
		m.cursor++
	}

	m.stopwatch, cmd = m.stopwatch.Update(msg)
	cmds = append(cmds, cmd)

	return m, tea.Batch(cmds...)
}

func (m Model) View() string {
	var sb strings.Builder
	sb.WriteString(m.styles.typed_text_style.Render(m.prompt[:m.cursor]))
	sb.WriteString(m.styles.cursor_style.Render(string(m.prompt[m.cursor])))
	sb.WriteString(m.styles.untyped_text_style.Render(m.prompt[m.cursor+1:]))
	sb.WriteString(fmt.Sprintf("\ncursor_coord: %d", m.cursor))

	res := lipgloss.JoinVertical(lipgloss.Center, m.styles.global_style.Render(sb.String()), m.stopwatch.View())
	return lipgloss.PlaceHorizontal(m.width, lipgloss.Center, res)
}

Expected behavior
Cursor advances, all the text rendered before it is rendered normally, all the text after it is rendered fainted

Screenshots

Screencast.from.2024-11-06.23-01-18.webm

Additional context
At first glance this appears to be a lipgloss bug, but when I tried to replicate it with a minimal program, like so

package main

import (
	"fmt"

	"github.com/charmbracelet/lipgloss"
)

// styles are defined here is a similar manner

const prompt = "A quick brown fox jumps over the lazy dog"

func main() {
	dstyles := default_styles(10)
	cursor_coord := 15
	styled_text := ""

	styled_text += dstyles.typed_text_style.Render(prompt[:cursor_coord])
	styled_text += dstyles.cursor_style.Render(string(prompt[cursor_coord]))
	styled_text += dstyles.untyped_text_style.Render(prompt[cursor_coord+1:])

	fmt.Println(dstyles.global_style.Render(styled_text))
}

The bug did not replicate, everything rendered as expected.

Also, setting the cursor to a static value also didn't work in the above code example also did not replicate the bug. Setting the global_style to not add the .Width() also did not replicate the bug. So this is somewhere between lipgloss.Style.Width and the rendering procedure of bubbletea

@M0hammadUsman
Copy link

M0hammadUsman commented Nov 7, 2024

The width is the issue I have an application that is now breaking due to 1.2.0 update, on 1.1.2 everything was ok.
I am using Windows Terminal.

To replicate,

type testModel struct {
	w, h int
}

func (m testModel) Init() tea.Cmd {
	return nil
}

func (m testModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.w = msg.Width
		m.h = msg.Height
	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "q":
			return m, tea.Quit
		}
	}
	return m, nil
}

func (m testModel) View() string {
	c := lipgloss.NewStyle().
		Border(lipgloss.RoundedBorder(), true).
		BorderForeground(lipgloss.Color("#FFFDF5"))
	return c.
		Width(m.w - c.GetHorizontalFrameSize()).
		Height(m.h - c.GetVerticalFrameSize()).
		Render()
}

func main() {
	if _, err := tea.NewProgram(testModel{}, tea.WithAltScreen()).Run(); err != nil {
		log.Fatal(err)
	}
}

Run it with BubbleTea 1.1.2 and 1.2.0 and you will see the issue, hopefully.
Try to resize the window.

Should I open a new issue, or is it valid in this issue

aymanbagabas added a commit that referenced this issue Nov 8, 2024
…width

When the cursor reaches the end of the line, any escape sequences that
follow will only affect the last cell of the line. This is why we only
erase the rest of the line when the line is shorter than the width of
the terminal.

Fixes: #1225
Fixes: 0cef3c7 (feat(render): remove flickering)
@aymanbagabas
Copy link
Member

Hi @M0hammadUsman, I've looked into this and found a bug in the renderer. I've opened a new PR #1227 to fix this issue.

@M0hammadUsman
Copy link

@aymanbagabas Thank you so much, everything is now back to normal.

@aymanbagabas
Copy link
Member

@M0hammadUsman This is now fixed in v1.2.1. Thank you for reporting this issue 🙂

@ipsavitsky
Copy link
Author

ipsavitsky commented Nov 8, 2024

@aymanbagabas
Unfortunately, the code in the original issue still has the same artifact 😢

EDIT: I also downgraded to 1.1.2 and have the same bug

@M0hammadUsman
Copy link

@ipsavitsky

The issue happens when the text after the cursor wraps around, try this example below

package main

import (
	"fmt"
	"strings"
	"time"

	"github.com/charmbracelet/bubbles/stopwatch"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

// Styles

var (
	terminalWidth, terminalHeight int

	containerStyle = lipgloss.NewStyle().
			Border(lipgloss.RoundedBorder()).
			Padding(1, 2)

	typedTextStyle = lipgloss.NewStyle()

	untypedTextStyle = typedTextStyle.Faint(true)

	cursorStyle = typedTextStyle.Reverse(true)
)

func main() {
	prompt := strings.Repeat("the end is never ", 100)
	p := tea.NewProgram(initialModel(prompt), tea.WithAltScreen())
	p.Run()
}

type Model struct {
	prompt    string
	cursor    int
	stopwatch stopwatch.Model
}

func initialModel(prompt string) Model {
	return Model{
		prompt:    prompt,
		stopwatch: stopwatch.NewWithInterval(100 * time.Millisecond),
	}
}

func (m Model) Init() tea.Cmd {
	return m.stopwatch.Start()
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmds []tea.Cmd
	var cmd tea.Cmd
	switch msg := msg.(type) {

	case tea.WindowSizeMsg:
		terminalWidth = msg.Width
		terminalHeight = msg.Height

	case tea.KeyMsg:
		switch msg.Type {
		case tea.KeyCtrlC, tea.KeyEsc:
			return m, tea.Quit
		}

	case stopwatch.TickMsg:
		m.cursor++
	}

	m.stopwatch, cmd = m.stopwatch.Update(msg)
	cmds = append(cmds, cmd)

	return m, tea.Batch(cmds...)
}

func (m Model) View() string {
	var sb strings.Builder
	sb.WriteString(typedTextStyle.Render(m.prompt[:m.cursor]))
	sb.WriteString(cursorStyle.Render(string(m.prompt[m.cursor])))
	sb.WriteString(untypedTextStyle.Render(" the issue happens when it wraps"))
	sb.WriteString(fmt.Sprintf("\ncursor_coord: %d", m.cursor))

	content := containerStyle.
		Width(terminalWidth - containerStyle.GetHorizontalFrameSize()).
		Render(sb.String())

	res := lipgloss.JoinVertical(lipgloss.Center, content, m.stopwatch.View())
	return lipgloss.Place(terminalWidth, terminalHeight, lipgloss.Center, lipgloss.Center, res)
}

@ipsavitsky
Copy link
Author

@M0hammadUsman

The issue happens when the text after the cursor wraps around, try this example below

Interesting, is there an easy way to work around this? Is there a way to get the first line and apply style only to it in lipgloss?

@M0hammadUsman
Copy link

M0hammadUsman commented Nov 8, 2024

I don't think so that there is a way, as the text have to wrap dynamically as the window resizes,

I've tried this to wrap the string myself, but no luck

lim := terminalWidth - (containerStyle.GetHorizontalFrameSize() + containerStyle.GetHorizontalPadding())
	content := containerStyle.
		Width(terminalWidth - containerStyle.GetHorizontalFrameSize()).
		Render(ansi.Wordwrap(sb.String(), lim, " "))

either the issue is with wrapping or the rendering, @aymanbagabas may help you with this.

EDIT: If you really want, you can remove the text that is rendered after the cursor altogether.

@ipsavitsky
Copy link
Author

@aymanbagabas
@meowgorithm
Can this please be reopened? The code in the original issue as well as the code provided by @M0hammadUsman in #1225 (comment) still has the bug. I am testing on bubbletea ver. 1.2.2 with the patch included. I have also tried to fix this myself but to no luck so far

@meowgorithm
Copy link
Member

meowgorithm commented Nov 19, 2024

Hi all! Happy to reopen. That said, I'm trying to understand the exact issue(s). Is it basically that when text wraps inside a box with a border styling on text breaks?

@meowgorithm meowgorithm reopened this Nov 19, 2024
@ipsavitsky
Copy link
Author

IIUC it doesn't have to be with a border, just wrapping text. Here is a more another example of the same bug (code derived from @M0hammadUsman's comment):

package main

import (
	"fmt"
	"strings"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

var (
	terminalWidth, terminalHeight int

	containerStyle = lipgloss.NewStyle().
			Border(lipgloss.RoundedBorder()).
			Padding(1, 2)

	typedTextStyle = lipgloss.NewStyle()

	untypedTextStyle = typedTextStyle.Faint(true)

	cursorStyle = typedTextStyle.Reverse(true)
)

func main() {
	prompt := strings.Repeat("the end is never ", 100)
	p := tea.NewProgram(initialModel(prompt), tea.WithAltScreen())
	p.Run()
}

type Model struct {
	prompt    string
	cursor    int
}

func initialModel(prompt string) Model {
	return Model{
		prompt:    prompt,
	}
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmds []tea.Cmd
	var cmd tea.Cmd
	switch msg := msg.(type) {

	case tea.WindowSizeMsg:
		terminalWidth = msg.Width
		terminalHeight = msg.Height

	case tea.KeyMsg:
		switch msg.Type {
		case tea.KeyCtrlC, tea.KeyEsc:
			return m, tea.Quit
		case tea.KeyLeft:
			m.cursor--
		case tea.KeyRight:
			m.cursor++
		}
	}
	cmds = append(cmds, cmd)

	return m, tea.Batch(cmds...)
}

func (m Model) View() string {
	var sb strings.Builder
	sb.WriteString(typedTextStyle.Render(m.prompt[:m.cursor]))
	sb.WriteString(cursorStyle.Render(string(m.prompt[m.cursor])))
	sb.WriteString(untypedTextStyle.Render(" the issue happens when it wraps"))
	sb.WriteString(fmt.Sprintf("\ncursor_coord: %d", m.cursor))

	content := containerStyle.
		Width(50).
		Render(sb.String())

	return lipgloss.Place(terminalWidth, terminalHeight, lipgloss.Center, lipgloss.Center, content)
}

The following happens when text starts to wrap:

Screencast.from.2024-11-19.14-41-40.webm

@M0hammadUsman
Copy link

M0hammadUsman commented Nov 19, 2024

Hi all! Happy to reopen. That said, I'm trying to understand the exact issue(s). Is it basically that when text wraps inside a box with a border styling on text breaks?

This is what I've found, run this example code

package main

import (
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

func main() {
	p := tea.NewProgram(Model{}, tea.WithAltScreen())
	p.Run()
}

type Model struct {
	width, height int
}

func (m Model) Init() tea.Cmd {
	return nil
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.width = msg.Width
		m.height = msg.Height

	case tea.KeyMsg:
		switch msg.Type {
		case tea.KeyCtrlC, tea.KeyEsc:
			return m, tea.Quit
		}
	}
	return m, nil
}

func (m Model) View() string {
	text1WithStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFC700")).Render("It breaks when it has to wrap, but the text it has to wrap")
	text2WithStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5C00")).Render("is a combination of two different lipgloss style renders.")
	text3WithStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#8b7000")).Render("\n\nDifferent colors make it obvious.")
	content := lipgloss.NewStyle().
		Border(lipgloss.RoundedBorder(), true).
		Padding(1, 3).
		Align(lipgloss.Center).
		Width(40).
		Render(text1WithStyle, text2WithStyle, text3WithStyle)
	return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, content)
}

Edit: Also try to add BorderForeground(lipgloss.Color("#FF5C00")) to the content, doing this renders the border correctly, but now the text breaks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants