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