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}