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}