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