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/theme"
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}
33
34func (c *container) Init() tea.Cmd {
35 return c.content.Init()
36}
37
38func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
39 u, cmd := c.content.Update(msg)
40 c.content = u
41 return c, cmd
42}
43
44func (c *container) View() string {
45 t := theme.CurrentTheme()
46 style := lipgloss.NewStyle()
47 width := c.width
48 height := c.height
49
50 style = style.Background(t.Background())
51
52 // Apply border if any side is enabled
53 if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
54 // Adjust width and height for borders
55 if c.borderTop {
56 height--
57 }
58 if c.borderBottom {
59 height--
60 }
61 if c.borderLeft {
62 width--
63 }
64 if c.borderRight {
65 width--
66 }
67 style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
68 style = style.BorderBackground(t.Background()).BorderForeground(t.BorderNormal())
69 }
70 style = style.
71 Width(width).
72 Height(height).
73 PaddingTop(c.paddingTop).
74 PaddingRight(c.paddingRight).
75 PaddingBottom(c.paddingBottom).
76 PaddingLeft(c.paddingLeft)
77
78 return style.Render(c.content.View())
79}
80
81func (c *container) SetSize(width, height int) tea.Cmd {
82 c.width = width
83 c.height = height
84
85 // If the content implements Sizeable, adjust its size to account for padding and borders
86 if sizeable, ok := c.content.(Sizeable); ok {
87 // Calculate horizontal space taken by padding and borders
88 horizontalSpace := c.paddingLeft + c.paddingRight
89 if c.borderLeft {
90 horizontalSpace++
91 }
92 if c.borderRight {
93 horizontalSpace++
94 }
95
96 // Calculate vertical space taken by padding and borders
97 verticalSpace := c.paddingTop + c.paddingBottom
98 if c.borderTop {
99 verticalSpace++
100 }
101 if c.borderBottom {
102 verticalSpace++
103 }
104
105 // Set content size with adjusted dimensions
106 contentWidth := max(0, width-horizontalSpace)
107 contentHeight := max(0, height-verticalSpace)
108 return sizeable.SetSize(contentWidth, contentHeight)
109 }
110 return nil
111}
112
113func (c *container) GetSize() (int, int) {
114 return c.width, c.height
115}
116
117func (c *container) BindingKeys() []key.Binding {
118 if b, ok := c.content.(Bindings); ok {
119 return b.BindingKeys()
120 }
121 return []key.Binding{}
122}
123
124type ContainerOption func(*container)
125
126func NewContainer(content tea.Model, options ...ContainerOption) Container {
127
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}