1package layout
  2
  3import (
  4	"log/slog"
  5
  6	"github.com/charmbracelet/bubbles/v2/key"
  7	tea "github.com/charmbracelet/bubbletea/v2"
  8
  9	"github.com/charmbracelet/crush/internal/tui/styles"
 10	"github.com/charmbracelet/crush/internal/tui/util"
 11	"github.com/charmbracelet/lipgloss/v2"
 12)
 13
 14type LayoutPanel string
 15
 16const (
 17	LeftPanel   LayoutPanel = "left"
 18	RightPanel  LayoutPanel = "right"
 19	BottomPanel LayoutPanel = "bottom"
 20)
 21
 22type SplitPaneLayout interface {
 23	util.Model
 24	Sizeable
 25	Help
 26	SetLeftPanel(panel Container) tea.Cmd
 27	SetRightPanel(panel Container) tea.Cmd
 28	SetBottomPanel(panel Container) tea.Cmd
 29
 30	ClearLeftPanel() tea.Cmd
 31	ClearRightPanel() tea.Cmd
 32	ClearBottomPanel() tea.Cmd
 33
 34	FocusPanel(panel LayoutPanel) tea.Cmd
 35	SetOffset(x, y int)
 36}
 37
 38type splitPaneLayout struct {
 39	width   int
 40	height  int
 41	xOffset int
 42	yOffset int
 43
 44	ratio         float64
 45	verticalRatio float64
 46
 47	rightPanel  Container
 48	leftPanel   Container
 49	bottomPanel Container
 50
 51	fixedBottomHeight int // Fixed height for the bottom panel, if any
 52	fixedRightWidth   int // Fixed width for the right panel, if any
 53}
 54
 55type SplitPaneOption func(*splitPaneLayout)
 56
 57func (s *splitPaneLayout) Init() tea.Cmd {
 58	var cmds []tea.Cmd
 59
 60	if s.leftPanel != nil {
 61		cmds = append(cmds, s.leftPanel.Init())
 62	}
 63
 64	if s.rightPanel != nil {
 65		cmds = append(cmds, s.rightPanel.Init())
 66	}
 67
 68	if s.bottomPanel != nil {
 69		cmds = append(cmds, s.bottomPanel.Init())
 70	}
 71
 72	return tea.Batch(cmds...)
 73}
 74
 75func (s *splitPaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 76	var cmds []tea.Cmd
 77	switch msg := msg.(type) {
 78	case tea.WindowSizeMsg:
 79		return s, s.SetSize(msg.Width, msg.Height)
 80	}
 81
 82	if s.rightPanel != nil {
 83		u, cmd := s.rightPanel.Update(msg)
 84		s.rightPanel = u.(Container)
 85		if cmd != nil {
 86			cmds = append(cmds, cmd)
 87		}
 88	}
 89
 90	if s.leftPanel != nil {
 91		u, cmd := s.leftPanel.Update(msg)
 92		s.leftPanel = u.(Container)
 93		if cmd != nil {
 94			cmds = append(cmds, cmd)
 95		}
 96	}
 97
 98	if s.bottomPanel != nil {
 99		u, cmd := s.bottomPanel.Update(msg)
100		s.bottomPanel = u.(Container)
101		if cmd != nil {
102			cmds = append(cmds, cmd)
103		}
104	}
105
106	return s, tea.Batch(cmds...)
107}
108
109func (s *splitPaneLayout) Cursor() *tea.Cursor {
110	if s.bottomPanel != nil {
111		if c, ok := s.bottomPanel.(util.Cursor); ok {
112			return c.Cursor()
113		}
114	} else if s.rightPanel != nil {
115		if c, ok := s.rightPanel.(util.Cursor); ok {
116			return c.Cursor()
117		}
118	} else if s.leftPanel != nil {
119		if c, ok := s.leftPanel.(util.Cursor); ok {
120			return c.Cursor()
121		}
122	}
123	return nil
124}
125
126func (s *splitPaneLayout) View() string {
127	var topSection string
128
129	if s.leftPanel != nil && s.rightPanel != nil {
130		leftView := s.leftPanel.View()
131		rightView := s.rightPanel.View()
132		topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView, rightView)
133	} else if s.leftPanel != nil {
134		topSection = s.leftPanel.View()
135	} else if s.rightPanel != nil {
136		topSection = s.rightPanel.View()
137	} else {
138		topSection = ""
139	}
140
141	var finalView string
142
143	if s.bottomPanel != nil && topSection != "" {
144		bottomView := s.bottomPanel.View()
145		finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView)
146	} else if s.bottomPanel != nil {
147		finalView = s.bottomPanel.View()
148	} else {
149		finalView = topSection
150	}
151
152	t := styles.CurrentTheme()
153
154	style := t.S().Base.
155		Width(s.width).
156		Height(s.height)
157
158	return style.Render(finalView)
159}
160
161func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
162	s.width = width
163	s.height = height
164	slog.Info("Setting split pane size", "width", width, "height", height)
165
166	var topHeight, bottomHeight int
167	var cmds []tea.Cmd
168	if s.bottomPanel != nil {
169		if s.fixedBottomHeight > 0 {
170			bottomHeight = s.fixedBottomHeight
171			topHeight = height - bottomHeight
172		} else {
173			topHeight = int(float64(height) * s.verticalRatio)
174			bottomHeight = height - topHeight
175			if bottomHeight <= 0 {
176				bottomHeight = 2
177				topHeight = height - bottomHeight
178			}
179		}
180	} else {
181		topHeight = height
182		bottomHeight = 0
183	}
184
185	var leftWidth, rightWidth int
186	if s.leftPanel != nil && s.rightPanel != nil {
187		if s.fixedRightWidth > 0 {
188			rightWidth = s.fixedRightWidth
189			leftWidth = width - rightWidth
190		} else {
191			leftWidth = int(float64(width) * s.ratio)
192			rightWidth = width - leftWidth
193			if rightWidth <= 0 {
194				rightWidth = 2
195				leftWidth = width - rightWidth
196			}
197		}
198	} else if s.leftPanel != nil {
199		leftWidth = width
200		rightWidth = 0
201	} else if s.rightPanel != nil {
202		leftWidth = 0
203		rightWidth = width
204	}
205
206	if s.leftPanel != nil {
207		cmd := s.leftPanel.SetSize(leftWidth, topHeight)
208		cmds = append(cmds, cmd)
209		if positional, ok := s.leftPanel.(Positional); ok {
210			cmds = append(cmds, positional.SetPosition(s.xOffset, s.yOffset))
211		}
212	}
213
214	if s.rightPanel != nil {
215		cmd := s.rightPanel.SetSize(rightWidth, topHeight)
216		cmds = append(cmds, cmd)
217		if positional, ok := s.rightPanel.(Positional); ok {
218			cmds = append(cmds, positional.SetPosition(s.xOffset+leftWidth, s.yOffset))
219		}
220	}
221
222	if s.bottomPanel != nil {
223		cmd := s.bottomPanel.SetSize(width, bottomHeight)
224		cmds = append(cmds, cmd)
225		if positional, ok := s.bottomPanel.(Positional); ok {
226			cmds = append(cmds, positional.SetPosition(s.xOffset, s.yOffset+topHeight))
227		}
228	}
229	return tea.Batch(cmds...)
230}
231
232func (s *splitPaneLayout) GetSize() (int, int) {
233	return s.width, s.height
234}
235
236func (s *splitPaneLayout) SetLeftPanel(panel Container) tea.Cmd {
237	s.leftPanel = panel
238	if s.width > 0 && s.height > 0 {
239		return s.SetSize(s.width, s.height)
240	}
241	return nil
242}
243
244func (s *splitPaneLayout) SetRightPanel(panel Container) tea.Cmd {
245	s.rightPanel = panel
246	if s.width > 0 && s.height > 0 {
247		return s.SetSize(s.width, s.height)
248	}
249	return nil
250}
251
252func (s *splitPaneLayout) SetBottomPanel(panel Container) tea.Cmd {
253	s.bottomPanel = panel
254	if s.width > 0 && s.height > 0 {
255		return s.SetSize(s.width, s.height)
256	}
257	return nil
258}
259
260func (s *splitPaneLayout) ClearLeftPanel() tea.Cmd {
261	s.leftPanel = nil
262	if s.width > 0 && s.height > 0 {
263		return s.SetSize(s.width, s.height)
264	}
265	return nil
266}
267
268func (s *splitPaneLayout) ClearRightPanel() tea.Cmd {
269	s.rightPanel = nil
270	if s.width > 0 && s.height > 0 {
271		return s.SetSize(s.width, s.height)
272	}
273	return nil
274}
275
276func (s *splitPaneLayout) ClearBottomPanel() tea.Cmd {
277	s.bottomPanel = nil
278	if s.width > 0 && s.height > 0 {
279		return s.SetSize(s.width, s.height)
280	}
281	return nil
282}
283
284func (s *splitPaneLayout) Bindings() []key.Binding {
285	if s.leftPanel != nil {
286		if b, ok := s.leftPanel.(Help); ok && s.leftPanel.IsFocused() {
287			return b.Bindings()
288		}
289	}
290	if s.rightPanel != nil {
291		if b, ok := s.rightPanel.(Help); ok && s.rightPanel.IsFocused() {
292			return b.Bindings()
293		}
294	}
295	if s.bottomPanel != nil {
296		if b, ok := s.bottomPanel.(Help); ok && s.bottomPanel.IsFocused() {
297			return b.Bindings()
298		}
299	}
300	return nil
301}
302
303func (s *splitPaneLayout) FocusPanel(panel LayoutPanel) tea.Cmd {
304	panels := map[LayoutPanel]Container{
305		LeftPanel:   s.leftPanel,
306		RightPanel:  s.rightPanel,
307		BottomPanel: s.bottomPanel,
308	}
309	var cmds []tea.Cmd
310	for p, container := range panels {
311		if container == nil {
312			continue
313		}
314		if p == panel {
315			cmds = append(cmds, container.Focus())
316		} else {
317			cmds = append(cmds, container.Blur())
318		}
319	}
320	return tea.Batch(cmds...)
321}
322
323// SetOffset implements SplitPaneLayout.
324func (s *splitPaneLayout) SetOffset(x int, y int) {
325	s.xOffset = x
326	s.yOffset = y
327}
328
329func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
330	layout := &splitPaneLayout{
331		ratio:         0.8,
332		verticalRatio: 0.92, // Default 90% for top section, 10% for bottom
333	}
334	for _, option := range options {
335		option(layout)
336	}
337	return layout
338}
339
340func WithLeftPanel(panel Container) SplitPaneOption {
341	return func(s *splitPaneLayout) {
342		s.leftPanel = panel
343	}
344}
345
346func WithRightPanel(panel Container) SplitPaneOption {
347	return func(s *splitPaneLayout) {
348		s.rightPanel = panel
349	}
350}
351
352func WithRatio(ratio float64) SplitPaneOption {
353	return func(s *splitPaneLayout) {
354		s.ratio = ratio
355	}
356}
357
358func WithBottomPanel(panel Container) SplitPaneOption {
359	return func(s *splitPaneLayout) {
360		s.bottomPanel = panel
361	}
362}
363
364func WithVerticalRatio(ratio float64) SplitPaneOption {
365	return func(s *splitPaneLayout) {
366		s.verticalRatio = ratio
367	}
368}
369
370func WithFixedBottomHeight(height int) SplitPaneOption {
371	return func(s *splitPaneLayout) {
372		s.fixedBottomHeight = height
373	}
374}
375
376func WithFixedRightWidth(width int) SplitPaneOption {
377	return func(s *splitPaneLayout) {
378		s.fixedRightWidth = width
379	}
380}