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

viewport line wrapping bug #644

Open
Kyren223 opened this issue Oct 21, 2024 · 5 comments
Open

viewport line wrapping bug #644

Kyren223 opened this issue Oct 21, 2024 · 5 comments
Assignees
Labels
question Further information is requested

Comments

@Kyren223
Copy link

Describe the bug
Multi-line (wrapped) text causes the viewport to not behave correctly, when using
viewport.Model#GotoBottom() and updating the height of it (by resizing the terminal),
Instead of hiding the top messages, it behaves weirdly and hides the bottom message.

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

  • OS Tumbleweed (through WSL2)
  • Shell zsh
  • Terminal Emulator Windows Terminal
  • Terminal Multiplexer tmux 3.4

To Reproduce
Steps to reproduce the behavior:

  1. Run the provided code
  2. Type a long message that doesn't fit in 1 line
  3. Type a few short messages that do fit in 1 line
  4. Resize the terminal height so the textarea + all the messages will fill the entire space
  5. Shrink the height by 1 char each time

Source Code
A slightly modified version of the example "chat" code

package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/charmbracelet/bubbles/textarea"
	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

func main() {
	p := tea.NewProgram(initialModel(), tea.WithAltScreen())

	if _, err := p.Run(); err != nil {
		log.Fatal(err)
	}
}

type (
	errMsg error
)

type model struct {
	viewport    viewport.Model
	messages    []string
	textarea    textarea.Model
	senderStyle lipgloss.Style
	err         error
}

func initialModel() model {
	ta := textarea.New()
	ta.Placeholder = "Send a message..."
	ta.Focus()

	ta.Prompt = "┃ "
	ta.CharLimit = 280

	ta.SetWidth(30)
	ta.SetHeight(3)

	// Remove cursor line styling
	ta.FocusedStyle.CursorLine = lipgloss.NewStyle()

	ta.ShowLineNumbers = false

	vp := viewport.New(30, 5)
	vp.SetContent(`Welcome to the chat room!
Type a message and press Enter to send.`)

	ta.KeyMap.InsertNewline.SetEnabled(false)

	return model{
		textarea:    ta,
		messages:    []string{},
		viewport:    vp,
		senderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("5")),
		err:         nil,
	}
}

func (m model) Init() tea.Cmd {
	return textarea.Blink
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var (
		tiCmd tea.Cmd
		vpCmd tea.Cmd
	)

	m.textarea, tiCmd = m.textarea.Update(msg)
	m.viewport, vpCmd = m.viewport.Update(msg)

	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.viewport.Width = msg.Width
		m.viewport.Height = msg.Height - m.textarea.Height()
		m.viewport.GotoBottom()

	case tea.KeyMsg:
		switch msg.Type {
		case tea.KeyCtrlC, tea.KeyEsc:
			fmt.Println(m.textarea.Value())
			return m, tea.Quit
		case tea.KeyEnter:
			m.messages = append(m.messages, m.senderStyle.Render("You: ")+m.textarea.Value())
			m.viewport.SetContent(strings.Join(m.messages, "\n"))
			m.textarea.Reset()
			m.viewport.GotoBottom()
		}

	// We handle errors just like any other message
	case errMsg:
		m.err = msg
		return m, nil
	}

	return m, tea.Batch(tiCmd, vpCmd)
}

func (m model) View() string {
	return fmt.Sprintf(
		"%s\n%s",
		m.viewport.View(),
		m.textarea.View(),
	) + ""
}

Expected behavior

  • First shrink hides the top line (first line of the long message)
  • Second shrink hides the top line (second line of the long message)
  • Third shrink hides the top line (of whatever the next line is)

Actual behavior

  • First shrink hides the bottom line
  • Second shrink hides the top line (first line of the long message)
  • Third shrink hides the top line (second line of the long message) AND re-shows the missing bottom line

Screenshots
Normal
image

After shrinking once (bottom line missing, both 1st and 2nd line of the top message exist)
The expected behaviour would be to either

  1. Hide the first line of the top message (preferred)
  2. Hide both the first and second line of the top message
    image

After shrinking again (bottom line reappears, both the 1st and 2nd line of the top message disappear)
image

@seeseemelk
Copy link

What a coincindence. I discovered this exact same issue while trying to diagnose a line wrapping issue in the Select widget from huh. (charmbracelet/huh#429)

@meowgorithm meowgorithm transferred this issue from charmbracelet/bubbletea Oct 22, 2024
@Kyren223
Copy link
Author

Glad to know this is actually an issue and not just a skill issue on my part, I discovered it while working on a TUI for messaging, so this is a core functionality of what I am making.
Hopefully this gets fixed fast

@meowgorithm
Copy link
Member

Hi! This is not entirely a bug: historically we've left wrapping as an exercise for the user. If you need to solve this now you can simply:

wrapped := lipgloss.NewStyle().Width(yourWidth).Render(yourContent)
viewport.SetContent(wrapped)

That said, we're tracking this in #578.

@bashbunni
Copy link
Member

Hey @Kyren223 this issue is more related to how we're handling content wrapping in our example rather than a bug with viewport. This is what to change from the chat example to have it work on window resize. I also added gap as a const so we wouldn't need magic numbers when calculating the available height.

diff --git a/examples/chat/main.go b/examples/chat/main.go
index 4a573d5..6b03a64 100644
--- a/examples/chat/main.go
+++ b/examples/chat/main.go
@@ -14,6 +14,8 @@ import (
 	"github.com/charmbracelet/lipgloss"
 )
 
+const gap = "\n\n"
+
 func main() {
 	p := tea.NewProgram(initialModel())
 
@@ -79,6 +81,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	m.viewport, vpCmd = m.viewport.Update(msg)
 
 	switch msg := msg.(type) {
+	case tea.WindowSizeMsg:
+		m.viewport.Width = msg.Width
+		m.textarea.SetWidth(msg.Width)
+		m.viewport.Height = msg.Height - m.textarea.Height() - lipgloss.Height(gap)
+
+		if len(m.messages) > 0 {
+			// Wrap content before setting it.
+			m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width).Render(strings.Join(m.messages, "\n")))
+		}
+		m.viewport.GotoBottom()
 	case tea.KeyMsg:
 		switch msg.Type {
 		case tea.KeyCtrlC, tea.KeyEsc:
@@ -86,7 +98,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			return m, tea.Quit
 		case tea.KeyEnter:
 			m.messages = append(m.messages, m.senderStyle.Render("You: ")+m.textarea.Value())
-			m.viewport.SetContent(strings.Join(m.messages, "\n"))
+			m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width).Render(strings.Join(m.messages, "\n")))
 			m.textarea.Reset()
 			m.viewport.GotoBottom()
 		}
@@ -102,8 +114,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 func (m model) View() string {
 	return fmt.Sprintf(
-		"%s\n\n%s",
+		"%s%s%s",
 		m.viewport.View(),
+		gap,
 		m.textarea.View(),
-	) + "\n\n"
+	)
 }

Let me know if you still run into any issues! Otherwise I will close this one.

@bashbunni bashbunni self-assigned this Dec 5, 2024
@bashbunni bashbunni added the question Further information is requested label Dec 5, 2024
@Kyren223
Copy link
Author

Kyren223 commented Dec 5, 2024

Hey, I understand not accepting the PR due to breaking changes, but I wouldn't classify this as a question, it's a bug.

If a component starts to behave weirdly (disappearing lines in a weird/unexpected way) when providing to it a string that has it's lipgloss width exceed the width of the component then I would call that a bug.

If wrapping it is a breaking change then at least making the behavior consistent (like truncating and not displaying anything that exceeds the width), or even better to have truncation as default and some opt-in method to set it to wrap instead of truncate.

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

Successfully merging a pull request may close this issue.

4 participants