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}