container.go

  1package layout
  2
  3import (
  4	"github.com/charmbracelet/bubbles/v2/key"
  5	tea "github.com/charmbracelet/bubbletea/v2"
  6	"github.com/charmbracelet/lipgloss/v2"
  7	"github.com/opencode-ai/opencode/internal/tui/theme"
  8	"github.com/opencode-ai/opencode/internal/tui/util"
  9)
 10
 11type Container interface {
 12	util.Model
 13	Sizeable
 14	Bindings
 15}
 16type container struct {
 17	width  int
 18	height int
 19
 20	content util.Model
 21
 22	// Style options
 23	paddingTop    int
 24	paddingRight  int
 25	paddingBottom int
 26	paddingLeft   int
 27
 28	borderTop    bool
 29	borderRight  bool
 30	borderBottom bool
 31	borderLeft   bool
 32	borderStyle  lipgloss.Border
 33}
 34
 35func (c *container) Init() tea.Cmd {
 36	return c.content.Init()
 37}
 38
 39func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 40	u, cmd := c.content.Update(msg)
 41	c.content = u.(util.Model)
 42	return c, cmd
 43}
 44
 45func (c *container) View() string {
 46	t := theme.CurrentTheme()
 47	style := lipgloss.NewStyle()
 48	width := c.width
 49	height := c.height
 50
 51	style = style.Background(t.Background())
 52
 53	// Apply border if any side is enabled
 54	if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
 55		// Adjust width and height for borders
 56		if c.borderTop {
 57			height--
 58		}
 59		if c.borderBottom {
 60			height--
 61		}
 62		if c.borderLeft {
 63			width--
 64		}
 65		if c.borderRight {
 66			width--
 67		}
 68		style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
 69		style = style.BorderBackground(t.Background()).BorderForeground(t.BorderNormal())
 70	}
 71	style = style.
 72		Width(width).
 73		Height(height).
 74		PaddingTop(c.paddingTop).
 75		PaddingRight(c.paddingRight).
 76		PaddingBottom(c.paddingBottom).
 77		PaddingLeft(c.paddingLeft)
 78
 79	return style.Render(c.content.View())
 80}
 81
 82func (c *container) SetSize(width, height int) tea.Cmd {
 83	c.width = width
 84	c.height = height
 85
 86	// If the content implements Sizeable, adjust its size to account for padding and borders
 87	if sizeable, ok := c.content.(Sizeable); ok {
 88		// Calculate horizontal space taken by padding and borders
 89		horizontalSpace := c.paddingLeft + c.paddingRight
 90		if c.borderLeft {
 91			horizontalSpace++
 92		}
 93		if c.borderRight {
 94			horizontalSpace++
 95		}
 96
 97		// Calculate vertical space taken by padding and borders
 98		verticalSpace := c.paddingTop + c.paddingBottom
 99		if c.borderTop {
100			verticalSpace++
101		}
102		if c.borderBottom {
103			verticalSpace++
104		}
105
106		// Set content size with adjusted dimensions
107		contentWidth := max(0, width-horizontalSpace)
108		contentHeight := max(0, height-verticalSpace)
109		return sizeable.SetSize(contentWidth, contentHeight)
110	}
111	return nil
112}
113
114func (c *container) GetSize() (int, int) {
115	return c.width, c.height
116}
117
118func (c *container) BindingKeys() []key.Binding {
119	if b, ok := c.content.(Bindings); ok {
120		return b.BindingKeys()
121	}
122	return []key.Binding{}
123}
124
125type ContainerOption func(*container)
126
127func NewContainer(content util.Model, options ...ContainerOption) Container {
128	c := &container{
129		content:     content,
130		borderStyle: lipgloss.NormalBorder(),
131	}
132
133	for _, option := range options {
134		option(c)
135	}
136
137	return c
138}
139
140// Padding options
141func WithPadding(top, right, bottom, left int) ContainerOption {
142	return func(c *container) {
143		c.paddingTop = top
144		c.paddingRight = right
145		c.paddingBottom = bottom
146		c.paddingLeft = left
147	}
148}
149
150func WithPaddingAll(padding int) ContainerOption {
151	return WithPadding(padding, padding, padding, padding)
152}
153
154func WithPaddingHorizontal(padding int) ContainerOption {
155	return func(c *container) {
156		c.paddingLeft = padding
157		c.paddingRight = padding
158	}
159}
160
161func WithPaddingVertical(padding int) ContainerOption {
162	return func(c *container) {
163		c.paddingTop = padding
164		c.paddingBottom = padding
165	}
166}
167
168func WithBorder(top, right, bottom, left bool) ContainerOption {
169	return func(c *container) {
170		c.borderTop = top
171		c.borderRight = right
172		c.borderBottom = bottom
173		c.borderLeft = left
174	}
175}
176
177func WithBorderAll() ContainerOption {
178	return WithBorder(true, true, true, true)
179}
180
181func WithBorderHorizontal() ContainerOption {
182	return WithBorder(true, false, true, false)
183}
184
185func WithBorderVertical() ContainerOption {
186	return WithBorder(false, true, false, true)
187}
188
189func WithBorderStyle(style lipgloss.Border) ContainerOption {
190	return func(c *container) {
191		c.borderStyle = style
192	}
193}
194
195func WithRoundedBorder() ContainerOption {
196	return WithBorderStyle(lipgloss.RoundedBorder())
197}
198
199func WithThickBorder() ContainerOption {
200	return WithBorderStyle(lipgloss.ThickBorder())
201}
202
203func WithDoubleBorder() ContainerOption {
204	return WithBorderStyle(lipgloss.DoubleBorder())
205}