split.go 5.5 KB

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