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) tea.Cmd
15 SetRightPanel(panel Container) tea.Cmd
16 SetBottomPanel(panel Container) tea.Cmd
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 return s, s.SetSize(msg.Width, msg.Height)
57 }
58
59 if s.rightPanel != nil {
60 u, cmd := s.rightPanel.Update(msg)
61 s.rightPanel = u.(Container)
62 if cmd != nil {
63 cmds = append(cmds, cmd)
64 }
65 }
66
67 if s.leftPanel != nil {
68 u, cmd := s.leftPanel.Update(msg)
69 s.leftPanel = u.(Container)
70 if cmd != nil {
71 cmds = append(cmds, cmd)
72 }
73 }
74
75 if s.bottomPanel != nil {
76 u, cmd := s.bottomPanel.Update(msg)
77 s.bottomPanel = u.(Container)
78 if cmd != nil {
79 cmds = append(cmds, cmd)
80 }
81 }
82
83 return s, tea.Batch(cmds...)
84}
85
86func (s *splitPaneLayout) View() string {
87 var topSection string
88
89 if s.leftPanel != nil && s.rightPanel != nil {
90 leftView := s.leftPanel.View()
91 rightView := s.rightPanel.View()
92 topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView, rightView)
93 } else if s.leftPanel != nil {
94 topSection = s.leftPanel.View()
95 } else if s.rightPanel != nil {
96 topSection = s.rightPanel.View()
97 } else {
98 topSection = ""
99 }
100
101 var finalView string
102
103 if s.bottomPanel != nil && topSection != "" {
104 bottomView := s.bottomPanel.View()
105 finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView)
106 } else if s.bottomPanel != nil {
107 finalView = s.bottomPanel.View()
108 } else {
109 finalView = topSection
110 }
111
112 if s.backgroundColor != nil && finalView != "" {
113 style := lipgloss.NewStyle().
114 Width(s.width).
115 Height(s.height).
116 Background(s.backgroundColor)
117
118 return style.Render(finalView)
119 }
120
121 return finalView
122}
123
124func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
125 s.width = width
126 s.height = height
127
128 var topHeight, bottomHeight int
129 if s.bottomPanel != nil {
130 topHeight = int(float64(height) * s.verticalRatio)
131 bottomHeight = height - topHeight
132 } else {
133 topHeight = height
134 bottomHeight = 0
135 }
136
137 var leftWidth, rightWidth int
138 if s.leftPanel != nil && s.rightPanel != nil {
139 leftWidth = int(float64(width) * s.ratio)
140 rightWidth = width - leftWidth
141 } else if s.leftPanel != nil {
142 leftWidth = width
143 rightWidth = 0
144 } else if s.rightPanel != nil {
145 leftWidth = 0
146 rightWidth = width
147 }
148
149 var cmds []tea.Cmd
150 if s.leftPanel != nil {
151 cmd := s.leftPanel.SetSize(leftWidth, topHeight)
152 cmds = append(cmds, cmd)
153 }
154
155 if s.rightPanel != nil {
156 cmd := s.rightPanel.SetSize(rightWidth, topHeight)
157 cmds = append(cmds, cmd)
158 }
159
160 if s.bottomPanel != nil {
161 cmd := s.bottomPanel.SetSize(width, bottomHeight)
162 cmds = append(cmds, cmd)
163 }
164 return tea.Batch(cmds...)
165}
166
167func (s *splitPaneLayout) GetSize() (int, int) {
168 return s.width, s.height
169}
170
171func (s *splitPaneLayout) SetLeftPanel(panel Container) tea.Cmd {
172 s.leftPanel = panel
173 if s.width > 0 && s.height > 0 {
174 return s.SetSize(s.width, s.height)
175 }
176 return nil
177}
178
179func (s *splitPaneLayout) SetRightPanel(panel Container) tea.Cmd {
180 s.rightPanel = panel
181 if s.width > 0 && s.height > 0 {
182 return s.SetSize(s.width, s.height)
183 }
184 return nil
185}
186
187func (s *splitPaneLayout) SetBottomPanel(panel Container) tea.Cmd {
188 s.bottomPanel = panel
189 if s.width > 0 && s.height > 0 {
190 return s.SetSize(s.width, s.height)
191 }
192 return nil
193}
194
195func (s *splitPaneLayout) BindingKeys() []key.Binding {
196 keys := []key.Binding{}
197 if s.leftPanel != nil {
198 if b, ok := s.leftPanel.(Bindings); ok {
199 keys = append(keys, b.BindingKeys()...)
200 }
201 }
202 if s.rightPanel != nil {
203 if b, ok := s.rightPanel.(Bindings); ok {
204 keys = append(keys, b.BindingKeys()...)
205 }
206 }
207 if s.bottomPanel != nil {
208 if b, ok := s.bottomPanel.(Bindings); ok {
209 keys = append(keys, b.BindingKeys()...)
210 }
211 }
212 return keys
213}
214
215func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
216 layout := &splitPaneLayout{
217 ratio: 0.7,
218 verticalRatio: 0.9, // Default 80% for top section, 20% for bottom
219 backgroundColor: styles.Background,
220 }
221 for _, option := range options {
222 option(layout)
223 }
224 return layout
225}
226
227func WithLeftPanel(panel Container) SplitPaneOption {
228 return func(s *splitPaneLayout) {
229 s.leftPanel = panel
230 }
231}
232
233func WithRightPanel(panel Container) SplitPaneOption {
234 return func(s *splitPaneLayout) {
235 s.rightPanel = panel
236 }
237}
238
239func WithRatio(ratio float64) SplitPaneOption {
240 return func(s *splitPaneLayout) {
241 s.ratio = ratio
242 }
243}
244
245func WithSplitBackgroundColor(color lipgloss.TerminalColor) SplitPaneOption {
246 return func(s *splitPaneLayout) {
247 s.backgroundColor = color
248 }
249}
250
251func WithBottomPanel(panel Container) SplitPaneOption {
252 return func(s *splitPaneLayout) {
253 s.bottomPanel = panel
254 }
255}
256
257func WithVerticalRatio(ratio float64) SplitPaneOption {
258 return func(s *splitPaneLayout) {
259 s.verticalRatio = ratio
260 }
261}