split.go

  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}