split.go

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