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 SplitPaneLayout interface {
11 tea.Model
12 Sizeable
13 Bindings
14 SetLeftPanel(panel Container) tea.Cmd
15 SetRightPanel(panel Container) tea.Cmd
16 SetBottomPanel(panel Container) tea.Cmd
17
18 ClearLeftPanel() tea.Cmd
19 ClearRightPanel() tea.Cmd
20 ClearBottomPanel() tea.Cmd
21}
22
23type splitPaneLayout struct {
24 width int
25 height int
26 ratio float64
27 verticalRatio float64
28
29 rightPanel Container
30 leftPanel Container
31 bottomPanel Container
32}
33
34type SplitPaneOption func(*splitPaneLayout)
35
36func (s *splitPaneLayout) Init() tea.Cmd {
37 var cmds []tea.Cmd
38
39 if s.leftPanel != nil {
40 cmds = append(cmds, s.leftPanel.Init())
41 }
42
43 if s.rightPanel != nil {
44 cmds = append(cmds, s.rightPanel.Init())
45 }
46
47 if s.bottomPanel != nil {
48 cmds = append(cmds, s.bottomPanel.Init())
49 }
50
51 return tea.Batch(cmds...)
52}
53
54func (s *splitPaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
55 var cmds []tea.Cmd
56 switch msg := msg.(type) {
57 case tea.WindowSizeMsg:
58 return s, s.SetSize(msg.Width, msg.Height)
59 }
60
61 if s.rightPanel != nil {
62 u, cmd := s.rightPanel.Update(msg)
63 s.rightPanel = u.(Container)
64 if cmd != nil {
65 cmds = append(cmds, cmd)
66 }
67 }
68
69 if s.leftPanel != nil {
70 u, cmd := s.leftPanel.Update(msg)
71 s.leftPanel = u.(Container)
72 if cmd != nil {
73 cmds = append(cmds, cmd)
74 }
75 }
76
77 if s.bottomPanel != nil {
78 u, cmd := s.bottomPanel.Update(msg)
79 s.bottomPanel = u.(Container)
80 if cmd != nil {
81 cmds = append(cmds, cmd)
82 }
83 }
84
85 return s, tea.Batch(cmds...)
86}
87
88func (s *splitPaneLayout) View() string {
89 var topSection string
90
91 if s.leftPanel != nil && s.rightPanel != nil {
92 leftView := s.leftPanel.View()
93 rightView := s.rightPanel.View()
94 topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView, rightView)
95 } else if s.leftPanel != nil {
96 topSection = s.leftPanel.View()
97 } else if s.rightPanel != nil {
98 topSection = s.rightPanel.View()
99 } else {
100 topSection = ""
101 }
102
103 var finalView string
104
105 if s.bottomPanel != nil && topSection != "" {
106 bottomView := s.bottomPanel.View()
107 finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView)
108 } else if s.bottomPanel != nil {
109 finalView = s.bottomPanel.View()
110 } else {
111 finalView = topSection
112 }
113
114 if finalView != "" {
115 t := theme.CurrentTheme()
116
117 style := lipgloss.NewStyle().
118 Width(s.width).
119 Height(s.height).
120 Background(t.Background())
121
122 return style.Render(finalView)
123 }
124
125 return finalView
126}
127
128func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
129 s.width = width
130 s.height = height
131
132 var topHeight, bottomHeight int
133 if s.bottomPanel != nil {
134 topHeight = int(float64(height) * s.verticalRatio)
135 bottomHeight = height - topHeight
136 } else {
137 topHeight = height
138 bottomHeight = 0
139 }
140
141 var leftWidth, rightWidth int
142 if s.leftPanel != nil && s.rightPanel != nil {
143 leftWidth = int(float64(width) * s.ratio)
144 rightWidth = width - leftWidth
145 } else if s.leftPanel != nil {
146 leftWidth = width
147 rightWidth = 0
148 } else if s.rightPanel != nil {
149 leftWidth = 0
150 rightWidth = width
151 }
152
153 var cmds []tea.Cmd
154 if s.leftPanel != nil {
155 cmd := s.leftPanel.SetSize(leftWidth, topHeight)
156 cmds = append(cmds, cmd)
157 }
158
159 if s.rightPanel != nil {
160 cmd := s.rightPanel.SetSize(rightWidth, topHeight)
161 cmds = append(cmds, cmd)
162 }
163
164 if s.bottomPanel != nil {
165 cmd := s.bottomPanel.SetSize(width, bottomHeight)
166 cmds = append(cmds, cmd)
167 }
168 return tea.Batch(cmds...)
169}
170
171func (s *splitPaneLayout) GetSize() (int, int) {
172 return s.width, s.height
173}
174
175func (s *splitPaneLayout) SetLeftPanel(panel Container) tea.Cmd {
176 s.leftPanel = panel
177 if s.width > 0 && s.height > 0 {
178 return s.SetSize(s.width, s.height)
179 }
180 return nil
181}
182
183func (s *splitPaneLayout) SetRightPanel(panel Container) tea.Cmd {
184 s.rightPanel = panel
185 if s.width > 0 && s.height > 0 {
186 return s.SetSize(s.width, s.height)
187 }
188 return nil
189}
190
191func (s *splitPaneLayout) SetBottomPanel(panel Container) tea.Cmd {
192 s.bottomPanel = panel
193 if s.width > 0 && s.height > 0 {
194 return s.SetSize(s.width, s.height)
195 }
196 return nil
197}
198
199func (s *splitPaneLayout) ClearLeftPanel() tea.Cmd {
200 s.leftPanel = nil
201 if s.width > 0 && s.height > 0 {
202 return s.SetSize(s.width, s.height)
203 }
204 return nil
205}
206
207func (s *splitPaneLayout) ClearRightPanel() tea.Cmd {
208 s.rightPanel = nil
209 if s.width > 0 && s.height > 0 {
210 return s.SetSize(s.width, s.height)
211 }
212 return nil
213}
214
215func (s *splitPaneLayout) ClearBottomPanel() tea.Cmd {
216 s.bottomPanel = nil
217 if s.width > 0 && s.height > 0 {
218 return s.SetSize(s.width, s.height)
219 }
220 return nil
221}
222
223func (s *splitPaneLayout) BindingKeys() []key.Binding {
224 keys := []key.Binding{}
225 if s.leftPanel != nil {
226 if b, ok := s.leftPanel.(Bindings); ok {
227 keys = append(keys, b.BindingKeys()...)
228 }
229 }
230 if s.rightPanel != nil {
231 if b, ok := s.rightPanel.(Bindings); ok {
232 keys = append(keys, b.BindingKeys()...)
233 }
234 }
235 if s.bottomPanel != nil {
236 if b, ok := s.bottomPanel.(Bindings); ok {
237 keys = append(keys, b.BindingKeys()...)
238 }
239 }
240 return keys
241}
242
243func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
244
245 layout := &splitPaneLayout{
246 ratio: 0.7,
247 verticalRatio: 0.9, // Default 90% for top section, 10% for bottom
248 }
249 for _, option := range options {
250 option(layout)
251 }
252 return layout
253}
254
255func WithLeftPanel(panel Container) SplitPaneOption {
256 return func(s *splitPaneLayout) {
257 s.leftPanel = panel
258 }
259}
260
261func WithRightPanel(panel Container) SplitPaneOption {
262 return func(s *splitPaneLayout) {
263 s.rightPanel = panel
264 }
265}
266
267func WithRatio(ratio float64) SplitPaneOption {
268 return func(s *splitPaneLayout) {
269 s.ratio = ratio
270 }
271}
272
273func WithBottomPanel(panel Container) SplitPaneOption {
274 return func(s *splitPaneLayout) {
275 s.bottomPanel = panel
276 }
277}
278
279func WithVerticalRatio(ratio float64) SplitPaneOption {
280 return func(s *splitPaneLayout) {
281 s.verticalRatio = ratio
282 }
283}