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/termai/internal/tui/styles"
  8)
  9
 10type SplitPaneLayout interface {
 11	tea.Model
 12	Sizeable
 13	SetLeftPanel(panel Container)
 14	SetRightPanel(panel Container)
 15	SetBottomPanel(panel Container)
 16}
 17
 18type splitPaneLayout struct {
 19	width         int
 20	height        int
 21	ratio         float64
 22	verticalRatio float64
 23
 24	rightPanel  Container
 25	leftPanel   Container
 26	bottomPanel Container
 27
 28	backgroundColor lipgloss.TerminalColor
 29}
 30
 31type SplitPaneOption func(*splitPaneLayout)
 32
 33func (s *splitPaneLayout) Init() tea.Cmd {
 34	var cmds []tea.Cmd
 35
 36	if s.leftPanel != nil {
 37		cmds = append(cmds, s.leftPanel.Init())
 38	}
 39
 40	if s.rightPanel != nil {
 41		cmds = append(cmds, s.rightPanel.Init())
 42	}
 43
 44	if s.bottomPanel != nil {
 45		cmds = append(cmds, s.bottomPanel.Init())
 46	}
 47
 48	return tea.Batch(cmds...)
 49}
 50
 51func (s *splitPaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 52	var cmds []tea.Cmd
 53	switch msg := msg.(type) {
 54	case tea.WindowSizeMsg:
 55		s.SetSize(msg.Width, msg.Height)
 56		return s, nil
 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) {
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	if s.leftPanel != nil {
150		s.leftPanel.SetSize(leftWidth, topHeight)
151	}
152
153	if s.rightPanel != nil {
154		s.rightPanel.SetSize(rightWidth, topHeight)
155	}
156
157	if s.bottomPanel != nil {
158		s.bottomPanel.SetSize(width, bottomHeight)
159	}
160}
161
162func (s *splitPaneLayout) GetSize() (int, int) {
163	return s.width, s.height
164}
165
166func (s *splitPaneLayout) SetLeftPanel(panel Container) {
167	s.leftPanel = panel
168	if s.width > 0 && s.height > 0 {
169		s.SetSize(s.width, s.height)
170	}
171}
172
173func (s *splitPaneLayout) SetRightPanel(panel Container) {
174	s.rightPanel = panel
175	if s.width > 0 && s.height > 0 {
176		s.SetSize(s.width, s.height)
177	}
178}
179
180func (s *splitPaneLayout) SetBottomPanel(panel Container) {
181	s.bottomPanel = panel
182	if s.width > 0 && s.height > 0 {
183		s.SetSize(s.width, s.height)
184	}
185}
186
187func (s *splitPaneLayout) BindingKeys() []key.Binding {
188	keys := []key.Binding{}
189	if s.leftPanel != nil {
190		if b, ok := s.leftPanel.(Bindings); ok {
191			keys = append(keys, b.BindingKeys()...)
192		}
193	}
194	if s.rightPanel != nil {
195		if b, ok := s.rightPanel.(Bindings); ok {
196			keys = append(keys, b.BindingKeys()...)
197		}
198	}
199	if s.bottomPanel != nil {
200		if b, ok := s.bottomPanel.(Bindings); ok {
201			keys = append(keys, b.BindingKeys()...)
202		}
203	}
204	return keys
205}
206
207func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
208	layout := &splitPaneLayout{
209		ratio:           0.7,
210		verticalRatio:   0.9, // Default 80% for top section, 20% for bottom
211		backgroundColor: styles.Background,
212	}
213	for _, option := range options {
214		option(layout)
215	}
216	return layout
217}
218
219func WithLeftPanel(panel Container) SplitPaneOption {
220	return func(s *splitPaneLayout) {
221		s.leftPanel = panel
222	}
223}
224
225func WithRightPanel(panel Container) SplitPaneOption {
226	return func(s *splitPaneLayout) {
227		s.rightPanel = panel
228	}
229}
230
231func WithRatio(ratio float64) SplitPaneOption {
232	return func(s *splitPaneLayout) {
233		s.ratio = ratio
234	}
235}
236
237func WithSplitBackgroundColor(color lipgloss.TerminalColor) SplitPaneOption {
238	return func(s *splitPaneLayout) {
239		s.backgroundColor = color
240	}
241}
242
243func WithBottomPanel(panel Container) SplitPaneOption {
244	return func(s *splitPaneLayout) {
245		s.bottomPanel = panel
246	}
247}
248
249func WithVerticalRatio(ratio float64) SplitPaneOption {
250	return func(s *splitPaneLayout) {
251		s.verticalRatio = ratio
252	}
253}