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