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