Ver Fonte

fix(tui): overlay border backgrounds

adamdottv há 8 meses atrás
pai
commit
4cdc86612c

+ 0 - 3
packages/tui/internal/components/chat/editor.go

@@ -110,9 +110,6 @@ func (m *editorComponent) Content() string {
 		PaddingTop(1).
 		PaddingBottom(1).
 		Background(t.BackgroundElement()).
-		Border(lipgloss.ThickBorder(), false, true).
-		BorderForeground(t.BackgroundElement()).
-		BorderBackground(t.Background()).
 		Render(textarea)
 
 	hint := base("enter") + muted(" send   ")

+ 0 - 8
packages/tui/internal/components/dialog/complete.go

@@ -6,7 +6,6 @@ import (
 	"github.com/charmbracelet/bubbles/v2/key"
 	"github.com/charmbracelet/bubbles/v2/textarea"
 	tea "github.com/charmbracelet/bubbletea/v2"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/sst/opencode/internal/app"
 	"github.com/sst/opencode/internal/components/list"
 	"github.com/sst/opencode/internal/styles"
@@ -203,13 +202,6 @@ func (c *completionDialogComponent) View() string {
 
 	return baseStyle.Padding(0, 0).
 		Background(t.BackgroundElement()).
-		Border(lipgloss.ThickBorder()).
-		BorderTop(false).
-		BorderBottom(false).
-		BorderRight(true).
-		BorderLeft(true).
-		BorderBackground(t.Background()).
-		BorderForeground(t.BackgroundElement()).
 		Width(c.width).
 		Render(c.list.View())
 }

+ 5 - 10
packages/tui/internal/components/modal/modal.go

@@ -100,9 +100,9 @@ func (m *Modal) Render(contentView string, background string) string {
 	if m.title != "" {
 		titleStyle := baseStyle.
 			Foreground(t.Primary()).
-			Bold(true)
+			Bold(true).
+			Padding(0, 1)
 
-		// titleView := titleStyle.Render(m.title)
 		escStyle := baseStyle.Foreground(t.TextMuted()).Bold(false)
 		escText := escStyle.Render("esc")
 
@@ -123,14 +123,7 @@ func (m *Modal) Render(contentView string, background string) string {
 		PaddingTop(1).
 		PaddingBottom(1).
 		PaddingLeft(2).
-		PaddingRight(2).
-		BorderStyle(lipgloss.ThickBorder()).
-		BorderLeft(true).
-		BorderRight(true).
-		BorderLeftForeground(t.BackgroundSubtle()).
-		BorderLeftBackground(t.Background()).
-		BorderRightForeground(t.BackgroundSubtle()).
-		BorderRightBackground(t.Background())
+		PaddingRight(2)
 
 	modalView := modalStyle.
 		Width(outerWidth).
@@ -150,5 +143,7 @@ func (m *Modal) Render(contentView string, background string) string {
 		row,
 		modalView,
 		background,
+		layout.WithOverlayBorder(),
+		layout.WithOverlayBorderColor(t.Primary()),
 	)
 }

+ 19 - 24
packages/tui/internal/components/toast/toast.go

@@ -93,12 +93,7 @@ func (tm *ToastManager) renderSingleToast(toast Toast) string {
 	baseStyle := styles.BaseStyle().
 		Background(t.BackgroundElement()).
 		Foreground(t.Text()).
-		Padding(1, 2).
-		BorderStyle(lipgloss.ThickBorder()).
-		BorderBackground(t.Background()).
-		BorderForeground(toast.Color).
-		BorderLeft(true).
-		BorderRight(true)
+		Padding(1, 2)
 
 	maxWidth := max(40, layout.Current.Viewport.Width/3)
 	contentMaxWidth := max(maxWidth-6, 20)
@@ -137,9 +132,7 @@ func (tm *ToastManager) View() string {
 		toastViews = append(toastViews, toastView+"\n")
 	}
 
-	t := theme.CurrentTheme()
-	content := lipgloss.JoinVertical(lipgloss.Right, toastViews...)
-	return lipgloss.NewStyle().Background(t.Background()).Render(content)
+	return strings.Join(toastViews, "\n")
 }
 
 // RenderOverlay renders the toasts as an overlay on the given background
@@ -151,38 +144,40 @@ func (tm *ToastManager) RenderOverlay(background string) string {
 	bgWidth := lipgloss.Width(background)
 	bgHeight := lipgloss.Height(background)
 	result := background
-	
+
 	// Start from top with 2 character padding
 	currentY := 2
-	
+
 	// Render each toast individually
 	for _, toast := range tm.toasts {
 		// Render individual toast
 		toastView := tm.renderSingleToast(toast)
 		toastWidth := lipgloss.Width(toastView)
 		toastHeight := lipgloss.Height(toastView)
-		
+
 		// Position at top-right with 2 character padding from right edge
-		x := bgWidth - toastWidth - 2
-		
-		// Ensure we don't go negative
-		if x < 0 {
-			x = 0
-		}
-		
+		x := max(bgWidth-toastWidth-4, 0)
+
 		// Check if toast fits vertically
-		if currentY + toastHeight > bgHeight - 2 {
+		if currentY+toastHeight > bgHeight-2 {
 			// No more room for toasts
 			break
 		}
-		
+
 		// Place this toast
-		result = layout.PlaceOverlay(x, currentY, toastView, result)
-		
+		result = layout.PlaceOverlay(
+			x,
+			currentY,
+			toastView,
+			result,
+			layout.WithOverlayBorder(),
+			layout.WithOverlayBorderColor(toast.Color),
+		)
+
 		// Move down for next toast (add 1 for spacing between toasts)
 		currentY += toastHeight + 1
 	}
-	
+
 	return result
 }
 

+ 248 - 15
packages/tui/internal/layout/overlay.go

@@ -1,9 +1,13 @@
 package layout
 
 import (
+	"fmt"
+	"regexp"
 	"strings"
+	"unicode/utf8"
 
 	"github.com/charmbracelet/lipgloss/v2"
+	"github.com/charmbracelet/lipgloss/v2/compat"
 	chAnsi "github.com/charmbracelet/x/ansi"
 	"github.com/muesli/ansi"
 	"github.com/muesli/reflow/truncate"
@@ -23,29 +27,58 @@ func getLines(s string) (lines []string, widest int) {
 	return lines, widest
 }
 
+// overlayOptions holds configuration for overlay rendering
+type overlayOptions struct {
+	whitespace  *whitespace
+	border      bool
+	borderColor *compat.AdaptiveColor
+}
+
+// OverlayOption sets options for overlay rendering
+type OverlayOption func(*overlayOptions)
+
 // PlaceOverlay places fg on top of bg.
 func PlaceOverlay(
 	x, y int,
 	fg, bg string,
-	opts ...WhitespaceOption,
+	opts ...OverlayOption,
 ) 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
+	// Parse options
+	options := &overlayOptions{
+		whitespace: &whitespace{},
+	}
+	for _, opt := range opts {
+		opt(options)
 	}
 
-	// TODO: allow placement outside of the bg box?
-	x = util.Clamp(x, 0, bgWidth-fgWidth)
-	y = util.Clamp(y, 0, bgHeight-fgHeight)
+	// Adjust for borders if enabled
+	if options.border {
+		// Add space for left and right borders
+		adjustedFgWidth := fgWidth + 2
+		// Adjust placement to account for borders
+		x = util.Clamp(x, 0, bgWidth-adjustedFgWidth)
+		y = util.Clamp(y, 0, bgHeight-fgHeight)
 
-	ws := &whitespace{}
-	for _, opt := range opts {
-		opt(ws)
+		// Pad all foreground lines to the same width for consistent borders
+		for i := range fgLines {
+			lineWidth := ansi.PrintableRuneWidth(fgLines[i])
+			if lineWidth < fgWidth {
+				fgLines[i] += strings.Repeat(" ", fgWidth-lineWidth)
+			}
+		}
+	} else {
+		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)
 	}
 
 	var b strings.Builder
@@ -59,25 +92,62 @@ func PlaceOverlay(
 		}
 
 		pos := 0
+
+		// Handle left side of the line up to the overlay
 		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))
+				b.WriteString(options.whitespace.render(x - pos))
 				pos = x
 			}
 		}
 
-		fgLine := fgLines[i-y]
-		b.WriteString(fgLine)
-		pos += ansi.PrintableRuneWidth(fgLine)
+		// Render the overlay content with optional borders
+		if options.border {
+			// Get the foreground line
+			fgLine := fgLines[i-y]
+			fgLineWidth := ansi.PrintableRuneWidth(fgLine)
+			
+			// Extract the styles at the border positions
+			leftStyle := getStyleAtPosition(bgLine, pos)
+			rightStyle := getStyleAtPosition(bgLine, pos + 1 + fgLineWidth)
+			
+			// Left border - combine background from original with border foreground
+			leftSeq := combineStyles(leftStyle, options.borderColor)
+			if leftSeq != "" {
+				b.WriteString(leftSeq)
+			}
+			b.WriteString("┃")
+			b.WriteString("\x1b[0m") // Reset all styles
+			pos++
+
+			// Content
+			b.WriteString(fgLine)
+			pos += fgLineWidth
 
+			// Right border - combine background from original with border foreground
+			rightSeq := combineStyles(rightStyle, options.borderColor)
+			if rightSeq != "" {
+				b.WriteString(rightSeq)
+			}
+			b.WriteString("┃")
+			b.WriteString("\x1b[0m") // Reset all styles
+			pos++
+		} else {
+			// No border, just render the content
+			fgLine := fgLines[i-y]
+			b.WriteString(fgLine)
+			pos += ansi.PrintableRuneWidth(fgLine)
+		}
+
+		// Handle right side of the line after the overlay
 		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(options.whitespace.render(bgWidth - rightWidth - pos))
 		}
 
 		b.WriteString(right)
@@ -92,6 +162,146 @@ func cutLeft(s string, cutWidth int) string {
 	return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
 }
 
+// ansiStyle represents parsed ANSI style attributes
+type ansiStyle struct {
+	fgColor string
+	bgColor string
+	attrs   []string
+}
+
+// parseANSISequence parses an ANSI escape sequence into its components
+func parseANSISequence(seq string) ansiStyle {
+	style := ansiStyle{}
+	
+	// Extract the parameters from the sequence (e.g., \x1b[38;5;123;48;5;456m -> "38;5;123;48;5;456")
+	if !strings.HasPrefix(seq, "\x1b[") || !strings.HasSuffix(seq, "m") {
+		return style
+	}
+	
+	params := seq[2 : len(seq)-1]
+	if params == "" {
+		return style
+	}
+	
+	parts := strings.Split(params, ";")
+	i := 0
+	for i < len(parts) {
+		switch parts[i] {
+		case "0": // Reset
+			style = ansiStyle{}
+		case "1", "2", "3", "4", "5", "6", "7", "8", "9": // Various attributes
+			style.attrs = append(style.attrs, parts[i])
+		case "38": // Foreground color
+			if i+1 < len(parts) && parts[i+1] == "5" && i+2 < len(parts) {
+				// 256 color mode
+				style.fgColor = strings.Join(parts[i:i+3], ";")
+				i += 2
+			} else if i+1 < len(parts) && parts[i+1] == "2" && i+4 < len(parts) {
+				// RGB color mode
+				style.fgColor = strings.Join(parts[i:i+5], ";")
+				i += 4
+			}
+		case "48": // Background color
+			if i+1 < len(parts) && parts[i+1] == "5" && i+2 < len(parts) {
+				// 256 color mode
+				style.bgColor = strings.Join(parts[i:i+3], ";")
+				i += 2
+			} else if i+1 < len(parts) && parts[i+1] == "2" && i+4 < len(parts) {
+				// RGB color mode
+				style.bgColor = strings.Join(parts[i:i+5], ";")
+				i += 4
+			}
+		case "30", "31", "32", "33", "34", "35", "36", "37": // Standard foreground colors
+			style.fgColor = parts[i]
+		case "40", "41", "42", "43", "44", "45", "46", "47": // Standard background colors
+			style.bgColor = parts[i]
+		case "90", "91", "92", "93", "94", "95", "96", "97": // Bright foreground colors
+			style.fgColor = parts[i]
+		case "100", "101", "102", "103", "104", "105", "106", "107": // Bright background colors
+			style.bgColor = parts[i]
+		}
+		i++
+	}
+	
+	return style
+}
+
+// combineStyles creates an ANSI sequence that combines background from one style with foreground from another
+func combineStyles(bgStyle ansiStyle, fgColor *compat.AdaptiveColor) string {
+	if fgColor == nil && bgStyle.bgColor == "" && len(bgStyle.attrs) == 0 {
+		return ""
+	}
+	
+	var parts []string
+	
+	// Add attributes
+	parts = append(parts, bgStyle.attrs...)
+	
+	// Add background color from the original style
+	if bgStyle.bgColor != "" {
+		parts = append(parts, bgStyle.bgColor)
+	}
+	
+	// Add foreground color if specified
+	if fgColor != nil {
+		// Use the light color (could be improved to detect terminal background)
+		color := (*fgColor).Light
+		
+		// Use RGBA to get color components
+		r, g, b, _ := color.RGBA()
+		// RGBA returns 16-bit values, we need 8-bit
+		parts = append(parts, fmt.Sprintf("38;2;%d;%d;%d", r>>8, g>>8, b>>8))
+	}
+	
+	if len(parts) == 0 {
+		return ""
+	}
+	
+	return fmt.Sprintf("\x1b[%sm", strings.Join(parts, ";"))
+}
+
+// getStyleAtPosition extracts the active ANSI style at a given visual position
+func getStyleAtPosition(s string, targetPos int) ansiStyle {
+	// ANSI escape sequence regex
+	ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
+	
+	visualPos := 0
+	currentStyle := ansiStyle{}
+	
+	i := 0
+	for i < len(s) && visualPos <= targetPos {
+		// Check if we're at an ANSI escape sequence
+		if match := ansiRegex.FindStringIndex(s[i:]); match != nil && match[0] == 0 {
+			// Found an ANSI sequence at current position
+			seq := s[i : i+match[1]]
+			parsedStyle := parseANSISequence(seq)
+			
+			// Update current style (merge with existing)
+			if parsedStyle.fgColor != "" {
+				currentStyle.fgColor = parsedStyle.fgColor
+			}
+			if parsedStyle.bgColor != "" {
+				currentStyle.bgColor = parsedStyle.bgColor
+			}
+			if len(parsedStyle.attrs) > 0 {
+				currentStyle.attrs = parsedStyle.attrs
+			}
+			
+			i += match[1]
+		} else if i < len(s) {
+			// Regular character
+			if visualPos == targetPos {
+				return currentStyle
+			}
+			_, size := utf8.DecodeRuneInString(s[i:])
+			i += size
+			visualPos++
+		}
+	}
+	
+	return currentStyle
+}
+
 type whitespace struct {
 	style termenv.Style
 	chars string
@@ -129,3 +339,26 @@ func (w whitespace) render(width int) string {
 
 // WhitespaceOption sets a styling rule for rendering whitespace.
 type WhitespaceOption func(*whitespace)
+
+// WithWhitespace sets whitespace options for the overlay
+func WithWhitespace(opts ...WhitespaceOption) OverlayOption {
+	return func(o *overlayOptions) {
+		for _, opt := range opts {
+			opt(o.whitespace)
+		}
+	}
+}
+
+// WithOverlayBorder enables border rendering for the overlay
+func WithOverlayBorder() OverlayOption {
+	return func(o *overlayOptions) {
+		o.border = true
+	}
+}
+
+// WithOverlayBorderColor sets the border color for the overlay
+func WithOverlayBorderColor(color compat.AdaptiveColor) OverlayOption {
+	return func(o *overlayOptions) {
+		o.borderColor = &color
+	}
+}