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) 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}