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