split.go 5.3 KB

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