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