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